Skip to content

Commit d21a1a9

Browse files
mariiaKraievskaadamsaghy
authored andcommitted
FINERACT-2354: First step - basic implementation of re-aging for Interest bearing loans - Default Behavior, interestRecalculation = true, without dueDate change
1 parent 23c1b2b commit d21a1a9

File tree

17 files changed

+407
-452
lines changed

17 files changed

+407
-452
lines changed

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

Lines changed: 0 additions & 318 deletions
Large diffs are not rendered by default.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1824,4 +1824,7 @@ public boolean hasContractTerminationTransaction() {
18241824
return getLoanTransactions().stream().anyMatch(t -> t.isContractTermination() && t.isNotReversed());
18251825
}
18261826

1827+
public boolean hasReAgingTransaction() {
1828+
return getLoanTransactions().stream().anyMatch(t -> t.isReAge() && t.isNotReversed());
1829+
}
18271830
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ public void handleRepaymentOrRecoveryOrWaiverTransaction(final Loan loan, final
157157
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
158158
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
159159
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
160-
|| loan.hasContractTerminationTransaction())) {
160+
|| loan.hasContractTerminationTransaction()
161+
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
161162
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
162163
}
163164
reprocessLoanTransactionsService.reprocessTransactions(loan);

fineract-progressive-loan-embeddable-schedule-generator/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGenerator.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,10 @@
3434
public class EmbeddableProgressiveLoanScheduleGenerator {
3535

3636
private final ProgressiveLoanScheduleGenerator scheduleGenerator;
37-
private final ScheduledDateGenerator scheduledDateGenerator;
38-
private final EMICalculator emiCalculator;
3937

4038
public EmbeddableProgressiveLoanScheduleGenerator() {
41-
this.emiCalculator = new ProgressiveEMICalculator();
42-
this.scheduledDateGenerator = new DefaultScheduledDateGenerator();
39+
final ScheduledDateGenerator scheduledDateGenerator = new DefaultScheduledDateGenerator();
40+
final EMICalculator emiCalculator = new ProgressiveEMICalculator(scheduledDateGenerator);
4341
this.scheduleGenerator = new ProgressiveLoanScheduleGenerator(scheduledDateGenerator, emiCalculator,
4442
new NoopInterestScheduleModelRepositoryWrapper());
4543
}

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

Lines changed: 204 additions & 58 deletions
Large diffs are not rendered by default.

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.apache.fineract.organisation.monetary.domain.Money;
2828
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
2929
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
30+
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
31+
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
3032
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
3133
import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails;
3234
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
@@ -140,4 +142,7 @@ Money getPeriodInterestTillDate(@NotNull ProgressiveLoanInterestScheduleModel sc
140142
* interest paused.
141143
*/
142144
void applyInterestPause(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate fromDate, LocalDate endDate);
145+
146+
void updateModelRepaymentPeriodsDuringReAge(ProgressiveLoanInterestScheduleModel scheduleModel, LoanTransaction loanTransaction,
147+
LoanApplicationTerms loanApplicationTerms, MathContext mc);
143148
}

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

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,19 @@
4040
import org.apache.fineract.infrastructure.core.service.MathUtil;
4141
import org.apache.fineract.organisation.monetary.data.CurrencyData;
4242
import org.apache.fineract.organisation.monetary.domain.Money;
43+
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
4344
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
4445
import org.apache.fineract.portfolio.common.domain.DaysInYearCustomStrategyType;
4546
import org.apache.fineract.portfolio.common.domain.DaysInYearType;
4647
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
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.LoanTransaction;
52+
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
53+
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
5054
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
55+
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
5156
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiAdjustment;
5257
import org.apache.fineract.portfolio.loanproduct.calc.data.EmiChangeOperation;
5358
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
@@ -66,6 +71,8 @@ public final class ProgressiveEMICalculator implements EMICalculator {
6671
private static final BigDecimal DIVISOR_100 = new BigDecimal("100");
6772
private static final BigDecimal ONE_WEEK_IN_DAYS = BigDecimal.valueOf(7);
6873

74+
private final ScheduledDateGenerator scheduledDateGenerator;
75+
6976
@Override
7077
@NotNull
7178
public ProgressiveLoanInterestScheduleModel generatePeriodInterestScheduleModel(@NotNull List<LoanScheduleModelRepaymentPeriod> periods,
@@ -559,6 +566,129 @@ private void calculateEMIValueAndRateFactorsForDecliningBalanceInterestMethod(fi
559566
}
560567
}
561568

569+
@Override
570+
public void updateModelRepaymentPeriodsDuringReAge(final ProgressiveLoanInterestScheduleModel scheduleModel,
571+
final LoanTransaction loanTransaction, final LoanApplicationTerms loanApplicationTerms, final MathContext mc) {
572+
final LoanReAgeParameter loanReAgeParameter = loanTransaction.getLoanReAgeParameter();
573+
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
574+
final LocalDate transactionDate = loanTransaction.getTransactionDate();
575+
final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods();
576+
577+
moveOutstandingAmountsFromPeriodsBeforeReAging(existingRepaymentPeriods, reAgingStartDate);
578+
579+
final LocalDate periodStartDate = calculateFirstReAgedPeriodStartDate(loanReAgeParameter);
580+
581+
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generateTemporaryReAgedScheduleModel(loanApplicationTerms,
582+
mc, periodStartDate, transactionDate);
583+
584+
mergeNewInterestScheduleModelWithExistingOne(scheduleModel, temporaryReAgedScheduleModel, loanTransaction);
585+
}
586+
587+
/**
588+
* * Merging the new temporary model of re-aged repayment periods and existing one together. After that recalculate
589+
* the balances of the updated model and also recalculate the EMI if the EMI of the last repayment period differs
590+
* significantly from other periods.
591+
*/
592+
private void mergeNewInterestScheduleModelWithExistingOne(final ProgressiveLoanInterestScheduleModel scheduleModel,
593+
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel, final LoanTransaction loanTransaction) {
594+
final List<RepaymentPeriod> newPeriods = temporaryReAgedScheduleModel.repaymentPeriods();
595+
596+
if (newPeriods.isEmpty()) {
597+
return;
598+
}
599+
600+
final List<RepaymentPeriod> existingRepaymentPeriods = scheduleModel.repaymentPeriods();
601+
final LocalDate reAgingStartDate = loanTransaction.getLoanReAgeParameter().getStartDate();
602+
603+
final Optional<RepaymentPeriod> firstExistingRepaymentPeriodOpt = existingRepaymentPeriods.stream()
604+
.filter(period -> period.getDueDate().equals(reAgingStartDate)).findFirst();
605+
606+
for (final RepaymentPeriod newPeriod : newPeriods) {
607+
final Optional<RepaymentPeriod> existingRepaymentPeriodOpt = existingRepaymentPeriods.stream().filter(
608+
period -> period.getFromDate().equals(newPeriod.getFromDate()) && period.getDueDate().equals(newPeriod.getDueDate()))
609+
.findFirst();
610+
Optional<RepaymentPeriod> previousExistingRepaymentPeriodOpt = Optional.empty();
611+
if (existingRepaymentPeriodOpt.isPresent() && firstExistingRepaymentPeriodOpt.isPresent()
612+
&& existingRepaymentPeriodOpt.get().equals(firstExistingRepaymentPeriodOpt.get())) {
613+
previousExistingRepaymentPeriodOpt = existingRepaymentPeriodOpt.get().getPrevious();
614+
}
615+
616+
final Money newPrincipal = newPeriod.getDuePrincipal();
617+
final Money newInterest = newPeriod.getDueInterest();
618+
619+
final RepaymentPeriod rp = RepaymentPeriod.create(
620+
previousExistingRepaymentPeriodOpt.orElseGet(existingRepaymentPeriods::getLast), newPeriod.getFromDate(),
621+
newPeriod.getDueDate(), newPrincipal.add(newInterest), MoneyHelper.getMathContext(),
622+
loanTransaction.getLoan().getLoanProductRelatedDetail());
623+
rp.setTotalDisbursedAmount(scheduleModel.repaymentPeriods().getFirst().getTotalDisbursedAmount());
624+
625+
existingRepaymentPeriodOpt.ifPresent(existingRepaymentPeriods::remove);
626+
existingRepaymentPeriods.add(rp);
627+
calculateRateFactorForRepaymentPeriod(rp, scheduleModel);
628+
}
629+
630+
final RepaymentPeriod lastReAgedInstallment = newPeriods.getLast();
631+
final List<RepaymentPeriod> reAgedRepaymentPeriods = existingRepaymentPeriods.stream()
632+
.filter(repaymentPeriod -> (!repaymentPeriod.getFromDate().isBefore(reAgingStartDate)
633+
|| repaymentPeriod.getDueDate().isEqual(reAgingStartDate))
634+
&& !repaymentPeriod.getDueDate().isAfter(lastReAgedInstallment.getDueDate()))
635+
.toList();
636+
637+
calculateOutstandingBalance(scheduleModel);
638+
calculateLastUnpaidRepaymentPeriodEMI(scheduleModel, loanTransaction.getTransactionDate());
639+
checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, reAgedRepaymentPeriods);
640+
}
641+
642+
/**
643+
* * Generates temporary interestScheduleModel with re-aged repayment periods
644+
*/
645+
@NotNull
646+
private ProgressiveLoanInterestScheduleModel generateTemporaryReAgedScheduleModel(final LoanApplicationTerms loanApplicationTerms,
647+
final MathContext mc, final LocalDate periodStartDate, final LocalDate transactionDate) {
648+
final List<LoanScheduleModelRepaymentPeriod> expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc,
649+
periodStartDate, loanApplicationTerms, null);
650+
final ProgressiveLoanInterestScheduleModel temporaryReAgedScheduleModel = generatePeriodInterestScheduleModel(
651+
expectedRepaymentPeriods, loanApplicationTerms.toLoanProductRelatedDetailMinimumData(), null,
652+
loanApplicationTerms.getInstallmentAmountInMultiplesOf(), mc);
653+
654+
addDisbursement(temporaryReAgedScheduleModel, EmiChangeOperation.disburse(transactionDate, loanApplicationTerms.getPrincipal()));
655+
return temporaryReAgedScheduleModel;
656+
}
657+
658+
/**
659+
* * Based on the re-aging start date and frequency data calculates start date for the first re-aged period, which
660+
* is used to generate re-aged repayment periods
661+
*/
662+
@NotNull
663+
private static LocalDate calculateFirstReAgedPeriodStartDate(final LoanReAgeParameter loanReAgeParameter) {
664+
final LocalDate reAgingStartDate = loanReAgeParameter.getStartDate();
665+
return switch (loanReAgeParameter.getFrequencyType()) {
666+
case DAYS -> reAgingStartDate.minusDays(loanReAgeParameter.getFrequencyNumber());
667+
case WEEKS -> reAgingStartDate.minusWeeks(loanReAgeParameter.getFrequencyNumber());
668+
case MONTHS -> reAgingStartDate.minusMonths(loanReAgeParameter.getFrequencyNumber());
669+
case YEARS -> reAgingStartDate.minusYears(loanReAgeParameter.getFrequencyNumber());
670+
case WHOLE_TERM -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: WHOLE_TERM");
671+
case INVALID -> throw new IllegalStateException("Unexpected RecalculationFrequencyType: INVALID");
672+
};
673+
}
674+
675+
/**
676+
* * Zeroing out the EMI of the repayment periods, that are before re-aging and not been fully paid. And decreases
677+
* the balance correction amount (added during interest recalculation for the business date) by the amount of the
678+
* principal that was moved.
679+
*/
680+
private static void moveOutstandingAmountsFromPeriodsBeforeReAging(final List<RepaymentPeriod> existingRepaymentPeriods,
681+
final LocalDate reAgingStartDate) {
682+
final List<RepaymentPeriod> periodsBeforeReAging = existingRepaymentPeriods.stream()
683+
.filter(rp -> rp.getFromDate().isBefore(reAgingStartDate) && !rp.isFullyPaid()).toList();
684+
685+
periodsBeforeReAging.forEach(rp -> {
686+
final InterestPeriod lastInterestPeriod = rp.getInterestPeriods().getLast();
687+
lastInterestPeriod.addBalanceCorrectionAmount(rp.getOutstandingPrincipal().negated());
688+
rp.setEmi(rp.getTotalPaidAmount());
689+
});
690+
}
691+
562692
private void calculateLastUnpaidRepaymentPeriodEMI(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate tillDate) {
563693
Optional<RepaymentPeriod> findLastUnpaidRepaymentPeriod = scheduleModel.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid())
564694
.reduce((first, second) -> second);

fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/LoanScheduleGeneratorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
@ExtendWith(MockitoExtension.class)
4545
class LoanScheduleGeneratorTest {
4646

47-
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();
47+
private static final ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(mock(ScheduledDateGenerator.class));
4848
private static final ApplicationCurrency APPLICATION_CURRENCY = new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$");
4949
private static final CurrencyData CURRENCY = APPLICATION_CURRENCY.toData();
5050
private static final BigDecimal DISBURSEMENT_AMOUNT = BigDecimal.valueOf(192.22);

fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
package org.apache.fineract.portfolio.loanproduct.calc;
2020

21+
import static org.mockito.Mockito.mock;
22+
2123
import java.math.BigDecimal;
2224
import java.math.MathContext;
2325
import java.math.RoundingMode;
@@ -39,6 +41,7 @@
3941
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
4042
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
4143
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod;
44+
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator;
4245
import org.apache.fineract.portfolio.loanaccount.service.ProgressiveLoanInterestScheduleModelParserServiceGsonImpl;
4346
import org.apache.fineract.portfolio.loanproduct.calc.data.InterestPeriod;
4447
import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails;
@@ -64,7 +67,7 @@
6467
@ExtendWith(MockitoExtension.class)
6568
class ProgressiveEMICalculatorTest {
6669

67-
private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator();
70+
private static ProgressiveEMICalculator emiCalculator = new ProgressiveEMICalculator(mock(ScheduledDateGenerator.class));
6871

6972
private static MockedStatic<ThreadLocalContextUtil> threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class);
7073
private static MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);

fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -860,7 +860,8 @@ interestRefundTransaction, new TransactionCtx(loan.getCurrency(), loan.getRepaym
860860
if (loan.isCumulativeSchedule() && loan.isInterestBearingAndInterestRecalculationEnabled()) {
861861
loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO);
862862
} else if (loan.isProgressiveSchedule() && ((loan.hasChargeOffTransaction() && loan.hasAccelerateChargeOffStrategy())
863-
|| loan.hasContractTerminationTransaction())) {
863+
|| loan.hasContractTerminationTransaction()
864+
|| (loan.isInterestRecalculationEnabled() && loan.hasReAgingTransaction()))) {
864865
loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO);
865866
}
866867
loan.getLoanTransactions().add(refundTransaction);

0 commit comments

Comments
 (0)