Skip to content

Commit d3a22c0

Browse files
committed
FINERACT-2354: Re-aging: -Interest Handling Option: Equal amortization -- EMICalculator
1 parent e7ae6e5 commit d3a22c0

13 files changed

Lines changed: 298 additions & 240 deletions

File tree

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

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@
7474
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
7575
import org.apache.fineract.portfolio.fund.domain.Fund;
7676
import org.apache.fineract.portfolio.group.domain.Group;
77-
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
7877
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
7978
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
8079
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
@@ -1825,14 +1824,6 @@ public boolean hasContractTerminationTransaction() {
18251824
return getLoanTransactions().stream().anyMatch(t -> t.isContractTermination() && t.isNotReversed());
18261825
}
18271826

1828-
public LoanReAgeInterestHandlingType getReAgeInterestHandlingType() {
1829-
Optional<LoanTransaction> lastReAgeTransaction = loanTransactions.stream()//
1830-
.filter(LoanTransaction::isNotReversed)//
1831-
.filter(LoanTransaction::isReAge)//
1832-
.findFirst();//
1833-
return lastReAgeTransaction.map(loanTransaction -> loanTransaction.getLoanReAgeParameter().getInterestHandlingType()).orElse(null);
1834-
}
1835-
18361827
public boolean hasReAgingTransaction() {
18371828
return getLoanTransactions().stream().anyMatch(t -> t.isReAge() && t.isNotReversed());
18381829
}

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/delinquency/service/ProgressivePossibleNextRepaymentCalculationServiceImpl.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,7 @@ public BigDecimal calculateInterestRecalculationFutureOutstandingValue(Loan loan
5858
}
5959
List<LoanRepaymentScheduleInstallment> repaymentScheduleInstallments = loan.getRepaymentScheduleInstallments();
6060
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), repaymentScheduleInstallments, Set.of(),
61-
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), scheduleModel,
62-
loan.getReAgeInterestHandlingType());
61+
new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), scheduleModel);
6362
ctx.setChargedOff(loan.isChargedOff());
6463
ctx.setWrittenOff(loan.isClosedWrittenOff());
6564
ctx.setContractTerminated(loan.isContractTermination());

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

Lines changed: 160 additions & 126 deletions
Large diffs are not rendered by default.

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
3030
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
3131
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
32-
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeInterestHandlingType;
3332
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
3433
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
3534
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -46,27 +45,22 @@ public class ProgressiveTransactionCtx extends TransactionCtx {
4645
@Setter
4746
private boolean isWrittenOff = false;
4847
@Setter
49-
private LoanReAgeInterestHandlingType reAgeInterestHandlingType;
50-
@Setter
5148
private boolean isContractTerminated = false;
5249
@Setter
5350
private boolean isPrepayAttempt = false;
5451
private final List<LoanRepaymentScheduleInstallment> skipRepaymentScheduleInstallments = new ArrayList<>();
5552

5653
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
5754
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
58-
ProgressiveLoanInterestScheduleModel model, LoanReAgeInterestHandlingType reAgeInterestHandlingType) {
59-
this(currency, installments, charges, overpaymentHolder, changedTransactionDetail, model, Money.zero(currency),
60-
reAgeInterestHandlingType);
55+
ProgressiveLoanInterestScheduleModel model) {
56+
this(currency, installments, charges, overpaymentHolder, changedTransactionDetail, model, Money.zero(currency));
6157
}
6258

6359
public ProgressiveTransactionCtx(MonetaryCurrency currency, List<LoanRepaymentScheduleInstallment> installments,
6460
Set<LoanCharge> charges, MoneyHolder overpaymentHolder, ChangedTransactionDetail changedTransactionDetail,
65-
ProgressiveLoanInterestScheduleModel model, Money sumOfInterestRefundAmount,
66-
LoanReAgeInterestHandlingType reAgeInterestHandlingType) {
61+
ProgressiveLoanInterestScheduleModel model, Money sumOfInterestRefundAmount) {
6762
super(currency, installments, charges, overpaymentHolder, changedTransactionDetail);
6863
this.sumOfInterestRefundAmount = sumOfInterestRefundAmount;
6964
this.model = model;
70-
this.reAgeInterestHandlingType = reAgeInterestHandlingType;
7165
}
7266
}

fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/InterestScheduleModelRepositoryWrapperImpl.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,8 +111,7 @@ public Optional<ProgressiveLoanInterestScheduleModel> getSavedModel(Loan loan, L
111111
savedModel = extractModel(progressiveLoanModel);
112112
if (savedModel.isPresent() && progressiveLoanModel.get().getBusinessDate().isBefore(businessDate)) {
113113
ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
114-
Set.of(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), savedModel.get(),
115-
loan.getReAgeInterestHandlingType());
114+
Set.of(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), new ChangedTransactionDetail(), savedModel.get());
116115
ctx.setChargedOff(loan.isChargedOff());
117116
ctx.setWrittenOff(loan.isClosedWrittenOff());
118117
ctx.setContractTerminated(loan.isContractTermination());

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
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;
3031
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
3132
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
3233
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
3334
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
35+
import org.apache.fineract.portfolio.loanproduct.calc.data.EqualAmortizationValues;
3436
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
3537
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
3638
import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
@@ -147,9 +149,17 @@ Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel sc
147149
void updateModelRepaymentPeriodsDuringReAge(ProgressiveLoanInterestScheduleModel scheduleModel, LoanTransaction loanTransaction,
148150
LoanApplicationTerms loanApplicationTerms, MathContext mc);
149151

150-
OutstandingDetails precalculateReAgeEqualAmortizationAmount(ProgressiveLoanInterestScheduleModel interestSchedule, LocalDate transactionDate,
151-
LoanReAgeParameter reageParameter);
152+
OutstandingDetails precalculateReAgeEqualAmortizationAmount(ProgressiveLoanInterestScheduleModel interestSchedule,
153+
LocalDate transactionDate, LoanReAgeParameter reageParameter);
152154

153155
void reAgeEqualAmortization(ProgressiveLoanInterestScheduleModel interestSchedule, LocalDate transactionDate,
154-
LoanReAgeParameter reageParameter);
156+
LoanReAgeParameter reageParameter, Money feesPenaltiesOutstanding,
157+
EqualAmortizationValues feesPenaltiesEqualAmortizationValues);
158+
159+
EqualAmortizationValues calculateEqualAmortizationValues(Money totalOutstanding, Integer numberOfInstallments,
160+
Integer installmentAmountInMultiplesOf, MonetaryCurrency currency);
161+
162+
EqualAmortizationValues calculateAdjustedEqualAmortizationValues(Money outstanding, Money total,
163+
Money sumOfOtherEqualAmortizationValues, Integer numberOfInstallments, Integer installmentAmountInMultiplesOf,
164+
MonetaryCurrency currency);
155165
}

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

Lines changed: 57 additions & 28 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.organisation.monetary.domain.MoneyHelper;
4445
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
@@ -56,6 +57,7 @@
5657
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
5758
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiAdjustment;
5859
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiChangeOperation;
60+
import org.apache.fineract.portfolio.loanproduct.calc.data.EqualAmortizationValues;
5961
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
6062
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
6163
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
@@ -313,6 +315,31 @@ public void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, Loc
313315
}
314316
}
315317

318+
@Override
319+
public EqualAmortizationValues calculateEqualAmortizationValues(Money totalOutstanding, Integer numberOfInstallments,
320+
Integer installmentAmountInMultiplesOf, MonetaryCurrency currency) {
321+
if (totalOutstanding.isGreaterThanZero()) {
322+
Money equalMonthlyValue = totalOutstanding.dividedBy(numberOfInstallments, totalOutstanding.getMc());
323+
if (installmentAmountInMultiplesOf != null) {
324+
equalMonthlyValue = Money.roundToMultiplesOf(equalMonthlyValue, installmentAmountInMultiplesOf);
325+
}
326+
Money adjustmentForLastInstallment = totalOutstanding.minus(equalMonthlyValue.multipliedBy(numberOfInstallments));
327+
return new EqualAmortizationValues(equalMonthlyValue, adjustmentForLastInstallment);
328+
}
329+
return new EqualAmortizationValues(Money.zero(currency), Money.zero(currency));
330+
}
331+
332+
@Override
333+
public EqualAmortizationValues calculateAdjustedEqualAmortizationValues(Money outstanding, Money total,
334+
Money sumOfOtherEqualAmortizationValues, Integer numberOfInstallments, Integer installmentAmountInMultiplesOf,
335+
MonetaryCurrency currency) {
336+
EqualAmortizationValues calculatedEMI = calculateEqualAmortizationValues(total, numberOfInstallments,
337+
installmentAmountInMultiplesOf, currency);
338+
Money value = calculatedEMI.value().minus(sumOfOtherEqualAmortizationValues);
339+
Money adjust = outstanding.minus(value.multipliedBy(numberOfInstallments));
340+
return new EqualAmortizationValues(value, adjust);
341+
}
342+
316343
private Optional<RepaymentPeriod> getLatestNotLastOpenRepaymentPeriodBeforeDate(ProgressiveLoanInterestScheduleModel scheduleModel,
317344
LocalDate transactionDate) {
318345
List<RepaymentPeriod> unpaidRepaymentPeriods = scheduleModel.repaymentPeriods() //
@@ -420,8 +447,9 @@ public Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleM
420447
targetDate);
421448
RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
422449
return includeCreditedInterest ? repaymentPeriod.getCalculatedDueInterest()
423-
: repaymentPeriod.getCalculatedDueInterest().minus(repaymentPeriod.getReAgedInterest(), recalculatedScheduleModelTillDate.mc()).minus(repaymentPeriod.getCreditedInterest(),
424-
recalculatedScheduleModelTillDate.mc());
450+
: repaymentPeriod.getCalculatedDueInterest()
451+
.minus(repaymentPeriod.getReAgedInterest(), recalculatedScheduleModelTillDate.mc())
452+
.minus(repaymentPeriod.getCreditedInterest(), recalculatedScheduleModelTillDate.mc());
425453
}
426454

427455
@Override
@@ -1512,39 +1540,35 @@ private void accelerateMaturityDateTo(ProgressiveLoanInterestScheduleModel inter
15121540
}
15131541
}
15141542

1515-
private void updateEMIForReAgeEqualAmortization(List<RepaymentPeriod> repaymentPeriods, Money principal, Money interest) {
1516-
Money principalPortion = principal.dividedBy(repaymentPeriods.size());
1517-
Money principalAdjustment = principal.minus(principalPortion.multipliedBy(repaymentPeriods.size()));
1518-
Money interestPortion = interest.dividedBy(repaymentPeriods.size());
1519-
Money interestAdjustment = interest.minus(interestPortion.multipliedBy(repaymentPeriods.size()));
1543+
private void updateEMIForReAgeEqualAmortization(List<RepaymentPeriod> repaymentPeriods, Money principal, Money interest,
1544+
Money feesPenaltiesOutstanding, EqualAmortizationValues feesPenaltiesEqualAmortizationValues, MonetaryCurrency currency) {
1545+
EqualAmortizationValues interestEAV = calculateEqualAmortizationValues(interest, repaymentPeriods.size(), null, currency);
1546+
EqualAmortizationValues principalEAV = calculateAdjustedEqualAmortizationValues(principal,
1547+
principal.add(interest).add(feesPenaltiesOutstanding),
1548+
interestEAV.value().add(feesPenaltiesEqualAmortizationValues.value()), repaymentPeriods.size(), null, currency);
15201549
RepaymentPeriod last = repaymentPeriods.getLast();
15211550
repaymentPeriods.forEach(rp -> {
1522-
rp.setReAgedInterest(interestPortion);
1523-
if (last == rp) {
1524-
rp.setReAgedInterest(interestPortion.add(interestAdjustment));
1525-
Money newEmi = principalPortion.add(principalAdjustment);
1526-
rp.setEmi(newEmi);
1527-
rp.setOriginalEmi(newEmi);
1528-
} else {
1529-
rp.setEmi(principalPortion);
1530-
rp.setOriginalEmi(principalPortion);
1531-
}
1551+
boolean isLast = last == rp;
1552+
rp.setReAgedInterest(interestEAV.calculateValue(isLast));
1553+
Money emi = principalEAV.calculateValue(isLast);
1554+
rp.setEmi(emi);
1555+
rp.setOriginalEmi(emi);
15321556
});
15331557
}
15341558

15351559
@Override
1536-
public OutstandingDetails precalculateReAgeEqualAmortizationAmount(ProgressiveLoanInterestScheduleModel interestSchedule, LocalDate transactionDate,
1537-
LoanReAgeParameter reageParameter) {
1538-
return getOutstandingAmountsTillDate(interestSchedule,
1539-
reageParameter.getInterestHandlingType().equals(LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST)
1540-
? transactionDate
1541-
: interestSchedule.getMaturityDate());
1560+
public OutstandingDetails precalculateReAgeEqualAmortizationAmount(ProgressiveLoanInterestScheduleModel interestSchedule,
1561+
LocalDate transactionDate, LoanReAgeParameter reageParameter) {
1562+
return getOutstandingAmountsTillDate(interestSchedule,
1563+
reageParameter.getInterestHandlingType().equals(LoanReAgeInterestHandlingType.EQUAL_AMORTIZATION_PAYABLE_INTEREST)
1564+
? transactionDate
1565+
: interestSchedule.getMaturityDate());
15421566
}
15431567

1544-
15451568
@Override
15461569
public void reAgeEqualAmortization(ProgressiveLoanInterestScheduleModel interestSchedule, LocalDate transactionDate,
1547-
LoanReAgeParameter reageParameter) {
1570+
LoanReAgeParameter reageParameter, Money feesPenaltiesOutstanding,
1571+
EqualAmortizationValues feesPenaltiesEqualAmortizationValues) {
15481572
LocalDate originalMaturityDate = interestSchedule.getMaturityDate();
15491573
boolean isAfterOriginalMaturityDate = transactionDate.isAfter(originalMaturityDate);
15501574
List<RepaymentPeriod> reAgedRepaymentPeriods = new ArrayList<>(reageParameter.getNumberOfInstallments());
@@ -1559,6 +1583,9 @@ public void reAgeEqualAmortization(ProgressiveLoanInterestScheduleModel interest
15591583
// close all open repayment period while keep paid amounts
15601584
interestSchedule.repaymentPeriods().forEach(rp -> {
15611585
rp.setEmi(rp.getTotalPaidAmount());
1586+
// TODO only works for total unpaid credited interest portions
1587+
rp.getInterestPeriods().forEach(ip -> ip.addCreditedInterestAmount(ip.getCreditedInterest().negated()));
1588+
rp.getInterestPeriods().forEach(ip -> ip.addCreditedPrincipalAmount(ip.getCreditedPrincipal().negated()));
15621589
});
15631590

15641591
// stop calculate unrecognised interest at this point because all
@@ -1573,7 +1600,8 @@ public void reAgeEqualAmortization(ProgressiveLoanInterestScheduleModel interest
15731600
updateModelForReageEqualAmortization(interestSchedule, reageParameter, reAgedRepaymentPeriods, isAfterOriginalMaturityDate);
15741601

15751602
updateEMIForReAgeEqualAmortization(reAgedRepaymentPeriods, reAgeingAmounts.getOutstandingPrincipal(),
1576-
reAgeingAmounts.getOutstandingInterest());
1603+
reAgeingAmounts.getOutstandingInterest(), feesPenaltiesOutstanding, feesPenaltiesEqualAmortizationValues,
1604+
interestSchedule.zero().getCurrency());
15771605

15781606
calculateOutstandingBalance(interestSchedule);
15791607

@@ -1598,7 +1626,7 @@ private void updateModelForReageEqualAmortization(ProgressiveLoanInterestSchedul
15981626
firstReAgedPeriod.setDueDate(toDate);
15991627
firstReAgedPeriod.getLastInterestPeriod().setDueDate(toDate);
16001628
firstReAgedPeriod.setReAged(true);
1601-
firstReAgedPeriod.getPrevious().ifPresent(prev->prev.setNoUnrecognisedInterest(true));
1629+
firstReAgedPeriod.getPrevious().ifPresent(prev -> prev.setNoUnrecognisedInterest(true));
16021630
reAgedRepaymentPeriods.add(firstReAgedPeriod);
16031631

16041632
// insert remaining
@@ -1628,12 +1656,13 @@ private void createRepaymentPeriodForEarlyRepaidAmountsDuringReAgeing(Progressiv
16281656
targetPeriod.addPaidInterestAmount(paidInterestToAdd);
16291657
targetPeriod.addPaidPrincipalAmount(paidPrincipalToAdd);
16301658
targetPeriod.setEmi(targetPeriod.getTotalPaidAmount());
1659+
targetPeriod.setReAged(true);
1660+
targetPeriod.setReAgedEarlyRepaymentHolder(true);
16311661

16321662
RepaymentPeriod repaymentPeriodToInsert = RepaymentPeriod.create(targetPeriod, targetPeriod.getDueDate(),
16331663
interestSchedule.getMaturityDate(), interestSchedule.zero(), interestSchedule.mc(),
16341664
interestSchedule.loanProductRelatedDetail());
16351665
repaymentPeriodToInsert.setReAged(true);
1636-
repaymentPeriodToInsert.setReAgedEarlyRepaymentHolder(true);
16371666
interestSchedule.repaymentPeriods().add(repaymentPeriodToInsert);
16381667
}
16391668

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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+
}

0 commit comments

Comments
 (0)