From 803a575b04de949f24c83b2d211adb8303d8a4b8 Mon Sep 17 00:00:00 2001 From: Janos Meszaros Date: Wed, 4 Sep 2024 10:09:58 +0200 Subject: [PATCH] FINERACT-2114: EMI Calculator outstanding balance correction --- ...veLoanInterestRepaymentInterestPeriod.java | 13 +- ...ProgressiveLoanInterestRepaymentModel.java | 20 ++- .../loanproduct/calc/EMICalculator.java | 5 + .../calc/ProgressiveEMICalculator.java | 160 ++++++++++++------ .../calc/ProgressiveEMICalculatorTest.java | 92 ++++++++++ 5 files changed, 233 insertions(+), 57 deletions(-) diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentInterestPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentInterestPeriod.java index 2a5a04ecb..42235c756 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentInterestPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentInterestPeriod.java @@ -35,10 +35,11 @@ public class ProgressiveLoanInterestRepaymentInterestPeriod implements Comparabl private BigDecimal rateFactorMinus1; private Money disbursedAmount; + private Money correctionAmount; private Money interestDue; public ProgressiveLoanInterestRepaymentInterestPeriod(final ProgressiveLoanInterestRepaymentInterestPeriod period) { - this(period.fromDate, period.dueDate, period.rateFactorMinus1, period.disbursedAmount, period.interestDue); + this(period.fromDate, period.dueDate, period.rateFactorMinus1, period.disbursedAmount, period.correctionAmount, period.interestDue); } @Override @@ -47,6 +48,14 @@ public class ProgressiveLoanInterestRepaymentInterestPeriod implements Comparabl } public void addDisbursedAmount(final Money outstandingBalance) { - this.disbursedAmount = this.disbursedAmount.add(outstandingBalance); + if (outstandingBalance != null && !outstandingBalance.isZero()) { + this.disbursedAmount = this.disbursedAmount.add(outstandingBalance); + } + } + + public void addCorrectionAmount(final Money correctionAmount) { + if (correctionAmount != null && !correctionAmount.isZero()) { + this.correctionAmount = this.correctionAmount.add(correctionAmount); + } } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentModel.java index f39f808f3..f79e0bcdc 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestRepaymentModel.java @@ -32,27 +32,34 @@ public class ProgressiveLoanInterestRepaymentModel { private LinkedList interestPeriods; + private boolean isLastPeriod; + private Money equalMonthlyInstallment; private Money principalDue; private Money remainingBalance; + private Money outstandingBalance; public ProgressiveLoanInterestRepaymentModel(final LocalDate fromDate, final LocalDate dueDate, final Money equalMonthlyInstallment) { this.fromDate = fromDate; this.dueDate = dueDate; this.equalMonthlyInstallment = equalMonthlyInstallment; + this.isLastPeriod = false; final Money zeroAmount = Money.zero(equalMonthlyInstallment.getCurrency()); + this.outstandingBalance = zeroAmount; this.remainingBalance = zeroAmount; this.principalDue = zeroAmount; this.interestPeriods = new LinkedList<>(); - this.interestPeriods - .add(new ProgressiveLoanInterestRepaymentInterestPeriod(fromDate, dueDate, BigDecimal.ZERO, zeroAmount, zeroAmount)); + this.interestPeriods.add( + new ProgressiveLoanInterestRepaymentInterestPeriod(fromDate, dueDate, BigDecimal.ZERO, zeroAmount, zeroAmount, zeroAmount)); } public ProgressiveLoanInterestRepaymentModel(ProgressiveLoanInterestRepaymentModel repaymentModel) { this.fromDate = repaymentModel.fromDate; this.dueDate = repaymentModel.dueDate; + this.isLastPeriod = repaymentModel.isLastPeriod; this.equalMonthlyInstallment = repaymentModel.equalMonthlyInstallment; + this.outstandingBalance = repaymentModel.outstandingBalance; this.remainingBalance = repaymentModel.remainingBalance; this.principalDue = repaymentModel.principalDue; this.interestPeriods = new LinkedList<>(); @@ -76,7 +83,12 @@ public class ProgressiveLoanInterestRepaymentModel { .reduce(Money.zero(equalMonthlyInstallment.getCurrency()), Money::plus); } - public Money getOutstandingBalance() { - return remainingBalance.plus(principalDue); + public Money getCorrectionAmount() { + return interestPeriods.stream().map(ProgressiveLoanInterestRepaymentInterestPeriod::getCorrectionAmount) + .reduce(Money.zero(equalMonthlyInstallment.getCurrency()), Money::plus); + } + + public Money getCorrectedOutstandingBalance() { + return outstandingBalance.plus(getCorrectionAmount()); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java index 352f6bf70..c932f9b27 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/EMICalculator.java @@ -46,6 +46,11 @@ public interface EMICalculator { void changeInterestRate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate newInterestEffectiveDate, BigDecimal newInterestRate); + void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate, + Money balanceCorrectionAmount); + + Optional getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate date); + ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(ProgressiveLoanInterestScheduleModel scheduleModel); ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(ProgressiveLoanInterestScheduleModel scheduleModel, diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index 290cbab13..9110a3d31 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; @@ -41,6 +42,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLo import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.mapper.ProgressiveLoanInterestRepaymentModelMapper; +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; @Component @@ -61,6 +63,9 @@ public final class ProgressiveEMICalculator implements EMICalculator { interestRepaymentModelList .add(new ProgressiveLoanInterestRepaymentModel(period.periodFromDate(), period.periodDueDate(), zeroAmount)); } + if (!interestRepaymentModelList.isEmpty()) { + interestRepaymentModelList.get(interestRepaymentModelList.size() - 1).setLastPeriod(true); + } return new ProgressiveLoanInterestScheduleModel(interestRepaymentModelList, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); } @@ -76,24 +81,24 @@ public final class ProgressiveEMICalculator implements EMICalculator { .findFirst(); } - Optional findInterestRepaymentPeriodForDisbursement( - final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate) { - if (scheduleModel == null || disbursementDueDate == null) { + Optional findInterestRepaymentPeriodForBalanceChange( + final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate balanceChangeDate) { + if (scheduleModel == null || balanceChangeDate == null) { return Optional.empty(); } return scheduleModel.repayments().stream()// - .filter(repaymentPeriod -> !disbursementDueDate.isBefore(repaymentPeriod.getFromDate()) - && disbursementDueDate.isBefore(repaymentPeriod.getDueDate()))// + .filter(repaymentPeriod -> !balanceChangeDate.isBefore(repaymentPeriod.getFromDate()) + && (repaymentPeriod.isLastPeriod() || balanceChangeDate.isBefore(repaymentPeriod.getDueDate())))// .findFirst(); } - Optional findInterestPeriodForDisbursement( - final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate disbursementDueDate) { - if (repaymentPeriod == null || disbursementDueDate == null) { + Optional findInterestPeriodForBalanceChange( + final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate balanceChangeDate) { + if (repaymentPeriod == null || balanceChangeDate == null) { return Optional.empty(); } return repaymentPeriod.getInterestPeriods().stream()// - .filter(interestPeriod -> disbursementDueDate.isEqual(interestPeriod.getFromDate()))// + .filter(interestPeriod -> balanceChangeDate.isEqual(interestPeriod.getFromDate()))// .findFirst(); } @@ -124,41 +129,54 @@ public final class ProgressiveEMICalculator implements EMICalculator { @Override public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate, final Money disbursedAmount) { - final ProgressiveLoanInterestRepaymentModel repaymentPeriod = findInterestRepaymentPeriodForDisbursement(scheduleModel, - disbursementDueDate).orElse(null); - if (repaymentPeriod == null) { - return; - } - - var interestPeriodOptional = findInterestPeriodForDisbursement(repaymentPeriod, disbursementDueDate); - if (interestPeriodOptional.isPresent()) { - interestPeriodOptional.get().addDisbursedAmount(disbursedAmount); - } else { - insertInterestPeriod(repaymentPeriod, disbursementDueDate, disbursedAmount); - } - - calculateEMIValueAndRateFactors(repaymentPeriod.getDueDate(), scheduleModel); + changeOutstandingBalanceAndUpdateInterestPeriods(scheduleModel, disbursementDueDate, disbursedAmount, + Money.zero(disbursedAmount.getCurrency())) + .ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors(repaymentPeriod.getDueDate(), scheduleModel)); } - void insertInterestPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate disbursementDueDate, - final Money disbursedAmount) { - // disbursementDueDate is after disb.date because this case when disbursement date is different than interest + Optional changeOutstandingBalanceAndUpdateInterestPeriods( + final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate balanceChangeDate, final Money disbursedAmount, + final Money correctionAmount) { + return findInterestRepaymentPeriodForBalanceChange(scheduleModel, balanceChangeDate).stream().peek(repaymentPeriod -> { + var interestPeriodOptional = findInterestPeriodForBalanceChange(repaymentPeriod, balanceChangeDate); + if (interestPeriodOptional.isPresent()) { + interestPeriodOptional.get().addDisbursedAmount(disbursedAmount); + interestPeriodOptional.get().addCorrectionAmount(correctionAmount); + } else { + insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount); + } + }).findFirst(); + } + + void insertInterestPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate balanceChangeDate, + final Money disbursedAmount, final Money correctionAmount) { + // balanceChangeDate is after disb.date because this case when disbursement date is different then interest // period start date final ProgressiveLoanInterestRepaymentInterestPeriod previousInterestPeriod = repaymentPeriod.getInterestPeriods().stream() - .filter(interestPeriod -> disbursementDueDate.isAfter(interestPeriod.getFromDate()) - && disbursementDueDate.isBefore(interestPeriod.getDueDate()))// + .filter(balanceChangeRelatedPreviousInterestPeriod(repaymentPeriod, balanceChangeDate))// .findFirst()// .get();// - final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(disbursementDueDate, - previousInterestPeriod.getDueDate(), BigDecimal.ZERO, disbursedAmount, Money.zero(disbursedAmount.getCurrency())); + final boolean changeAfterLastRepaymentPeriod = repaymentPeriod.isLastPeriod() + && previousInterestPeriod.getDueDate().isEqual(repaymentPeriod.getDueDate()); + final LocalDate interestPeriodDueDate = changeAfterLastRepaymentPeriod ? balanceChangeDate.plusDays(1) + : previousInterestPeriod.getDueDate(); + final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(balanceChangeDate, interestPeriodDueDate, + BigDecimal.ZERO, disbursedAmount, correctionAmount, Money.zero(disbursedAmount.getCurrency())); - previousInterestPeriod.setDueDate(disbursementDueDate); + previousInterestPeriod.setDueDate(balanceChangeDate); repaymentPeriod.getInterestPeriods().add(interestPeriod); Collections.sort(repaymentPeriod.getInterestPeriods()); } + private static @NotNull Predicate balanceChangeRelatedPreviousInterestPeriod( + ProgressiveLoanInterestRepaymentModel repaymentPeriod, LocalDate balanceChangeDate) { + return interestPeriod -> balanceChangeDate.isAfter(interestPeriod.getFromDate()) + && (balanceChangeDate.isBefore(interestPeriod.getDueDate()) + || (repaymentPeriod.isLastPeriod() && !balanceChangeDate.isBefore(repaymentPeriod.getDueDate()))); + } + @Override public ProgressiveLoanInterestScheduleModel makeScheduleModelDeepCopy(final ProgressiveLoanInterestScheduleModel scheduleModel) { return makeScheduleModelDeepCopy(scheduleModel, scheduleModel.loanProductRelatedDetail(), @@ -203,7 +221,7 @@ public final class ProgressiveEMICalculator implements EMICalculator { final Money zeroAmount = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()); final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(interestChangeDueDate, - previousInterestPeriod.getDueDate(), BigDecimal.ZERO, zeroAmount, zeroAmount); + previousInterestPeriod.getDueDate(), BigDecimal.ZERO, zeroAmount, zeroAmount, zeroAmount); previousInterestPeriod.setDueDate(interestChangeDueDate); @@ -211,6 +229,30 @@ public final class ProgressiveEMICalculator implements EMICalculator { Collections.sort(repaymentPeriod.getInterestPeriods()); } + @Override + public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate, + Money balanceCorrectionAmount) { + final Money zeroAmount = Money.zero(balanceCorrectionAmount.getCurrency()); + changeOutstandingBalanceAndUpdateInterestPeriods(scheduleModel, balanceCorrectionDate, zeroAmount, balanceCorrectionAmount) + .ifPresent(repaymentPeriod -> { + calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel); + calculatePrincipalInterestComponentsForPeriods(scheduleModel); + }); + } + + @Override + public Optional getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel, + LocalDate date) { + final var newScheduleModel = makeScheduleModelDeepCopy(scheduleModel); + final var zeroAmount = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()); + + return changeOutstandingBalanceAndUpdateInterestPeriods(newScheduleModel, date, zeroAmount, zeroAmount).stream() + .peek(repaymentPeriod -> { + calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel); + calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, date); + }).findFirst(); + } + /** * Calculate Equal Monthly Installment value and Rate Factor -1 values for calculate Interest */ @@ -302,11 +344,13 @@ public final class ProgressiveEMICalculator implements EMICalculator { */ void calculateRateFactorMinus1ForPeriods(final List repaymentPeriods, final ProgressiveLoanInterestScheduleModel scheduleModel) { - for (var repaymentPeriod : repaymentPeriods) { - for (var interestPeriod : repaymentPeriod.getInterestPeriods()) { - interestPeriod.setRateFactorMinus1(calculateRateFactorMinus1PerPeriod(repaymentPeriod, interestPeriod, scheduleModel)); - } - } + repaymentPeriods.forEach(repaymentPeriod -> calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel)); + } + + void calculateRateFactorMinus1ForRepaymentPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, + final ProgressiveLoanInterestScheduleModel scheduleModel) { + repaymentPeriod.getInterestPeriods().forEach(interestPeriod -> interestPeriod + .setRateFactorMinus1(calculateRateFactorMinus1PerPeriod(repaymentPeriod, interestPeriod, scheduleModel))); } /** @@ -636,35 +680,45 @@ public final class ProgressiveEMICalculator implements EMICalculator { void calculatePrincipalInterestComponentsForPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel) { Money outstandingBalance = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()); - int numberOfPeriods = scheduleModel.repayments().size(); - int period = 0; for (var repaymentPeriod : scheduleModel.repayments()) { - final boolean isLastPeriod = ++period == numberOfPeriods; - outstandingBalance = calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, outstandingBalance, isLastPeriod); + repaymentPeriod.setOutstandingBalance(outstandingBalance); + calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, null); + outstandingBalance = repaymentPeriod.getRemainingBalance(); } } - Money calculatePrincipalInterestComponentsForPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, - final Money initBalance, final boolean isLastPeriod) { - Money outstandingBalance = initBalance; + void calculatePrincipalInterestComponentsForPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, + final LocalDate calculateTill) { + final Money zeroAmount = Money.zero(repaymentPeriod.getOutstandingBalance().getCurrency()); + Money outstandingBalance = repaymentPeriod.getOutstandingBalance(); + Money balanceCorrection = zeroAmount; + Money cumulatedInterest = zeroAmount; + for (ProgressiveLoanInterestRepaymentInterestPeriod interestPeriod : repaymentPeriod.getInterestPeriods()) { + final boolean shouldInvalidateInterestPeriod = calculateTill != null && interestPeriod.getDueDate().isAfter(calculateTill); + if (shouldInvalidateInterestPeriod) { + interestPeriod.setInterestDue(zeroAmount); + interestPeriod.setDisbursedAmount(zeroAmount); + interestPeriod.setCorrectionAmount(zeroAmount); + continue; + } outstandingBalance = outstandingBalance.plus(interestPeriod.getDisbursedAmount()); - final Money calculatedInterest = outstandingBalance.multipliedBy(interestPeriod.getRateFactorMinus1()); + balanceCorrection = balanceCorrection.plus(interestPeriod.getCorrectionAmount()); + final Money calculatedInterest = outstandingBalance.plus(balanceCorrection).multipliedBy(interestPeriod.getRateFactorMinus1()); interestPeriod.setInterestDue(calculatedInterest); + cumulatedInterest = cumulatedInterest.plus(calculatedInterest); } - final Money cumulatedInterest = repaymentPeriod.getInterestDue(); - final Money calculatedPrincipal = isLastPeriod ? outstandingBalance + final Money calculatedPrincipal = repaymentPeriod.isLastPeriod() ? outstandingBalance : repaymentPeriod.getEqualMonthlyInstallment().minus(cumulatedInterest); - if (isLastPeriod) { + if (repaymentPeriod.isLastPeriod()) { repaymentPeriod.setEqualMonthlyInstallment(calculatedPrincipal.add(cumulatedInterest)); } + final Money remainingBalance = outstandingBalance.minus(calculatedPrincipal); repaymentPeriod.setPrincipalDue(calculatedPrincipal); - repaymentPeriod.setRemainingBalance(outstandingBalance.minus(calculatedPrincipal)); - - return repaymentPeriod.getRemainingBalance(); + repaymentPeriod.setRemainingBalance(remainingBalance); } @Override @@ -674,6 +728,10 @@ public final class ProgressiveEMICalculator implements EMICalculator { .filter(period -> !period.isDownPayment() && !period.isAdditional()).toList(); List repaymentModels = progressiveLoanInterestRepaymentModelMapper .map(repaymentModelsWithoutDownPayment); + + if (!repaymentModels.isEmpty()) { + repaymentModels.get(repaymentModels.size() - 1).setLastPeriod(true); + } return new ProgressiveLoanInterestScheduleModel(repaymentModels, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); } } diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index 109bd4870..cca1df987 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -34,6 +34,7 @@ import org.apache.fineract.portfolio.common.domain.DaysInMonthType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestRepaymentModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; @@ -332,6 +333,97 @@ class ProgressiveEMICalculatorTest { checkPeriod(interestSchedule, 5, 0, 16.90, 0.003333333333, 0.06, 16.84, 0.0); } + /** + * This test case tests a period early and late repayment with balance correction + */ + @Test + public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_add_balance_on0215() { + final MathContext mc = MoneyHelper.getMathContext(); + final List 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 = new BigDecimal("7"); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getNominalInterestRatePerPeriod()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); + + threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 15)); + + final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generateInterestScheduleModel(expectedRepaymentPeriods, + loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); + + // partially pay off a period with balance correction + final LocalDate op1stCorrectionDate = LocalDate.of(2024, 2, 15); + final Money op1stCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(-83.57)); + + // get remaining balance and dues for a date + final ProgressiveLoanInterestRepaymentModel repaymentDetails1st = emiCalculator + .getPayableDetails(interestSchedule, op1stCorrectionDate).get(); + Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance().getAmount())); + Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPrincipalDue().getAmount())); + Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getInterestDue().getAmount())); + + emiCalculator.addBalanceCorrection(interestSchedule, op1stCorrectionDate, op1stCorrectionAmount); + + checkDisbursementOnPeriod(interestSchedule, 0, disbursedAmount); + checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 0.24, 16.77, 66.80); + checkPeriod(interestSchedule, 1, 1, 17.01, 0.003017241379, 0.0, 0.24, 16.77, 66.80); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0.39, 16.62, 50.18); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.46); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.20, 16.81, 16.65); + checkPeriod(interestSchedule, 5, 0, 16.75, 0.005833333333, 0.10, 16.65, 0.0); + + // totally pay off another period with balance correction + final LocalDate op2ndCorrectionDate = LocalDate.of(2024, 3, 1); + final Money op2ndCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(-66.80)); + + // get remaining balance and dues for a date + final ProgressiveLoanInterestRepaymentModel repaymentDetails2st = emiCalculator + .getPayableDetails(interestSchedule, op2ndCorrectionDate).get(); + Assertions.assertEquals(66.80, toDouble(repaymentDetails2st.getOutstandingBalance().getAmount())); + Assertions.assertEquals(17.01, toDouble(repaymentDetails2st.getPrincipalDue().getAmount())); + Assertions.assertEquals(0.0, toDouble(repaymentDetails2st.getInterestDue().getAmount())); + + emiCalculator.addBalanceCorrection(interestSchedule, op2ndCorrectionDate, op2ndCorrectionAmount); + + checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 0.24, 16.77, 66.80); + checkPeriod(interestSchedule, 1, 1, 17.01, 0.003017241379, 0.0, 0.24, 16.77, 66.80); + checkPeriod(interestSchedule, 2, 0, 17.01, 0.005833333333, 0, 17.01, 49.79); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0.29, 16.72, 33.07); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0.19, 16.82, 16.25); + checkPeriod(interestSchedule, 5, 0, 16.34, 0.005833333333, 0.09, 16.25, 0.0); + + // check numbers on last period due date + final ProgressiveLoanInterestRepaymentModel repaymentDetails3rd = emiCalculator + .getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 1)).get(); + Assertions.assertEquals(16.25, toDouble(repaymentDetails3rd.getOutstandingBalance().getAmount())); + Assertions.assertEquals(16.25, toDouble(repaymentDetails3rd.getPrincipalDue().getAmount())); + Assertions.assertEquals(0.09, toDouble(repaymentDetails3rd.getInterestDue().getAmount())); + + // check numbers after the last period due date + final ProgressiveLoanInterestRepaymentModel repaymentDetails4th = emiCalculator + .getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 15)).get(); + Assertions.assertEquals(16.25, toDouble(repaymentDetails4th.getOutstandingBalance().getAmount())); + Assertions.assertEquals(16.25, toDouble(repaymentDetails4th.getPrincipalDue().getAmount())); + Assertions.assertEquals(0.14, toDouble(repaymentDetails4th.getInterestDue().getAmount())); + } + // @Test // public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month_reschedule() { // final MathContext mc = MoneyHelper.getMathContext();