From 5215e565f7c4c7ae0c82e3b56c38fa0bcc5f3a18 Mon Sep 17 00:00:00 2001 From: Janos Meszaros Date: Fri, 6 Sep 2024 16:46:38 +0200 Subject: [PATCH] FINERACT-2114: Fix EMI Calculator get payment details --- ...ProgressiveLoanInterestRepaymentModel.java | 13 +- .../loanproduct/calc/EMICalculator.java | 3 +- .../calc/ProgressiveEMICalculator.java | 85 ++++++++---- .../calc/ProgressiveEMICalculatorTest.java | 124 +++++++++++++++++- 4 files changed, 192 insertions(+), 33 deletions(-) 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 f79e0bcdc77..7d1fa31077e 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 @@ -37,7 +37,8 @@ public class ProgressiveLoanInterestRepaymentModel { private Money equalMonthlyInstallment; private Money principalDue; private Money remainingBalance; - private Money outstandingBalance; + + private Money initialBalance; public ProgressiveLoanInterestRepaymentModel(final LocalDate fromDate, final LocalDate dueDate, final Money equalMonthlyInstallment) { this.fromDate = fromDate; @@ -46,7 +47,7 @@ public ProgressiveLoanInterestRepaymentModel(final LocalDate fromDate, final Loc this.isLastPeriod = false; final Money zeroAmount = Money.zero(equalMonthlyInstallment.getCurrency()); - this.outstandingBalance = zeroAmount; + this.initialBalance = zeroAmount; this.remainingBalance = zeroAmount; this.principalDue = zeroAmount; this.interestPeriods = new LinkedList<>(); @@ -59,7 +60,7 @@ public ProgressiveLoanInterestRepaymentModel(ProgressiveLoanInterestRepaymentMod this.dueDate = repaymentModel.dueDate; this.isLastPeriod = repaymentModel.isLastPeriod; this.equalMonthlyInstallment = repaymentModel.equalMonthlyInstallment; - this.outstandingBalance = repaymentModel.outstandingBalance; + this.initialBalance = repaymentModel.initialBalance; this.remainingBalance = repaymentModel.remainingBalance; this.principalDue = repaymentModel.principalDue; this.interestPeriods = new LinkedList<>(); @@ -88,7 +89,11 @@ public Money getCorrectionAmount() { .reduce(Money.zero(equalMonthlyInstallment.getCurrency()), Money::plus); } + public Money getOutstandingBalance() { + return initialBalance.plus(getDisbursedAmountInPeriod()); + } + public Money getCorrectedOutstandingBalance() { - return outstandingBalance.plus(getCorrectionAmount()); + return getOutstandingBalance().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 c932f9b277f..4ca096f272a 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 @@ -49,7 +49,8 @@ void changeInterestRate(ProgressiveLoanInterestScheduleModel scheduleModel, Loca void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate, Money balanceCorrectionAmount); - Optional getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate date); + Optional getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel, + LocalDate periodDueDate, LocalDate payDate); 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 9110a3d31fa..390052799d2 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 @@ -18,6 +18,7 @@ */ package org.apache.fineract.portfolio.loanproduct.calc; +import jakarta.validation.constraints.NotNull; import java.math.BigDecimal; import java.math.MathContext; import java.time.LocalDate; @@ -27,6 +28,7 @@ import java.util.Iterator; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Predicate; import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.core.service.DateUtils; @@ -42,7 +44,6 @@ 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 @@ -137,44 +138,81 @@ public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleM 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); + return findInterestRepaymentPeriodForBalanceChange(scheduleModel, balanceChangeDate).stream()// + .peek(updateInterestPeriodOnRepaymentPeriod(balanceChangeDate, disbursedAmount, correctionAmount, false))// + .findFirst();// + } + + @NotNull + private Consumer updateInterestPeriodOnRepaymentPeriod(final LocalDate balanceChangeDate, + final Money disbursedAmount, final Money correctionAmount, final boolean tillBalanceChangeDate) { + return repaymentPeriod -> { + if (tillBalanceChangeDate && balanceChangeDate.isEqual(repaymentPeriod.getFromDate())) { + insertInterestPeriodIntoStart(repaymentPeriod, disbursedAmount, correctionAmount); + return; + } + + final var interestPeriodOptional = findInterestPeriodForBalanceChange(repaymentPeriod, balanceChangeDate); if (interestPeriodOptional.isPresent()) { interestPeriodOptional.get().addDisbursedAmount(disbursedAmount); interestPeriodOptional.get().addCorrectionAmount(correctionAmount); } else { insertInterestPeriod(repaymentPeriod, balanceChangeDate, disbursedAmount, correctionAmount); } - }).findFirst(); + }; + } + + void insertInterestPeriodIntoStart(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final Money disbursedAmount, + final Money correctionAmount) { + final Money zeroAmount = Money.zero(disbursedAmount.getCurrency()); + // interestPeriodFromDate is after disb.date because this case when disbursement date is different then interest + // we always have at least one period + final ProgressiveLoanInterestRepaymentInterestPeriod selectedInterestPeriod = repaymentPeriod.getInterestPeriods().get(0); + + final LocalDate interestPeriodDueDate = selectedInterestPeriod.getFromDate(); + final var newInterestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(selectedInterestPeriod.getFromDate(), + interestPeriodDueDate, BigDecimal.ZERO, zeroAmount, zeroAmount, Money.zero(disbursedAmount.getCurrency())); + + newInterestPeriod.setDisbursedAmount(disbursedAmount.add(selectedInterestPeriod.getDisbursedAmount())); + newInterestPeriod.setCorrectionAmount(correctionAmount.add(selectedInterestPeriod.getCorrectionAmount())); + + // reset amounts on next periods + selectedInterestPeriod.setDisbursedAmount(zeroAmount); + selectedInterestPeriod.setCorrectionAmount(zeroAmount); + selectedInterestPeriod.setFromDate(interestPeriodDueDate); + + repaymentPeriod.getInterestPeriods().add(newInterestPeriod); + Collections.sort(repaymentPeriod.getInterestPeriods()); } - void insertInterestPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate balanceChangeDate, + void insertInterestPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate interestPeriodFromDate, final Money disbursedAmount, final Money correctionAmount) { - // balanceChangeDate is after disb.date because this case when disbursement date is different then interest + // interestPeriodFromDate is after disb.date because this case when disbursement date is different then interest // period start date final ProgressiveLoanInterestRepaymentInterestPeriod previousInterestPeriod = repaymentPeriod.getInterestPeriods().stream() - .filter(balanceChangeRelatedPreviousInterestPeriod(repaymentPeriod, balanceChangeDate))// + .filter(operationRelatedPreviousInterestPeriod(repaymentPeriod, interestPeriodFromDate))// .findFirst()// .get();// final boolean changeAfterLastRepaymentPeriod = repaymentPeriod.isLastPeriod() - && previousInterestPeriod.getDueDate().isEqual(repaymentPeriod.getDueDate()); - final LocalDate interestPeriodDueDate = changeAfterLastRepaymentPeriod ? balanceChangeDate.plusDays(1) + && previousInterestPeriod.getDueDate().isEqual(repaymentPeriod.getDueDate()) + && !interestPeriodFromDate.isBefore(repaymentPeriod.getDueDate()); + final LocalDate interestPeriodDueDate = changeAfterLastRepaymentPeriod ? interestPeriodFromDate.plusDays(1) : previousInterestPeriod.getDueDate(); - final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(balanceChangeDate, interestPeriodDueDate, + final var interestPeriod = new ProgressiveLoanInterestRepaymentInterestPeriod(interestPeriodFromDate, interestPeriodDueDate, BigDecimal.ZERO, disbursedAmount, correctionAmount, Money.zero(disbursedAmount.getCurrency())); - previousInterestPeriod.setDueDate(balanceChangeDate); + previousInterestPeriod.setDueDate(interestPeriodFromDate); 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()))); + private static Predicate operationRelatedPreviousInterestPeriod( + ProgressiveLoanInterestRepaymentModel repaymentPeriod, LocalDate operationDate) { + return interestPeriod -> operationDate.isAfter(interestPeriod.getFromDate()) + && (operationDate.isBefore(interestPeriod.getDueDate()) || (repaymentPeriod.getDueDate().equals(interestPeriod.getDueDate()) + && !operationDate.isBefore(repaymentPeriod.getDueDate()))); } @Override @@ -241,15 +279,16 @@ public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleMo } @Override - public Optional getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel, - LocalDate date) { + public Optional getPayableDetails(final ProgressiveLoanInterestScheduleModel scheduleModel, + final LocalDate periodDueDate, final LocalDate payDate) { final var newScheduleModel = makeScheduleModelDeepCopy(scheduleModel); final var zeroAmount = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()); - return changeOutstandingBalanceAndUpdateInterestPeriods(newScheduleModel, date, zeroAmount, zeroAmount).stream() + return findInterestRepaymentPeriod(newScheduleModel, periodDueDate).stream() + .peek(updateInterestPeriodOnRepaymentPeriod(payDate, zeroAmount, zeroAmount, true))// .peek(repaymentPeriod -> { calculateRateFactorMinus1ForRepaymentPeriod(repaymentPeriod, scheduleModel); - calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, date); + calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, payDate); }).findFirst(); } @@ -681,7 +720,7 @@ BigDecimal fnValue(final BigDecimal previousFnValue, final BigDecimal currentRat void calculatePrincipalInterestComponentsForPeriods(final ProgressiveLoanInterestScheduleModel scheduleModel) { Money outstandingBalance = Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency()); for (var repaymentPeriod : scheduleModel.repayments()) { - repaymentPeriod.setOutstandingBalance(outstandingBalance); + repaymentPeriod.setInitialBalance(outstandingBalance); calculatePrincipalInterestComponentsForPeriod(repaymentPeriod, null); outstandingBalance = repaymentPeriod.getRemainingBalance(); } @@ -689,8 +728,8 @@ void calculatePrincipalInterestComponentsForPeriods(final ProgressiveLoanInteres void calculatePrincipalInterestComponentsForPeriod(final ProgressiveLoanInterestRepaymentModel repaymentPeriod, final LocalDate calculateTill) { - final Money zeroAmount = Money.zero(repaymentPeriod.getOutstandingBalance().getCurrency()); - Money outstandingBalance = repaymentPeriod.getOutstandingBalance(); + final Money zeroAmount = Money.zero(repaymentPeriod.getInitialBalance().getCurrency()); + Money outstandingBalance = repaymentPeriod.getInitialBalance(); Money balanceCorrection = zeroAmount; Money cumulatedInterest = zeroAmount; 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 cca1df98715..35762a2cd0a 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 @@ -337,7 +337,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay * This test case tests a period early and late repayment with balance correction */ @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_add_balance_on0215() { + public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_add_balance_correction_on0215() { final MathContext mc = MoneyHelper.getMathContext(); final List expectedRepaymentPeriods = new ArrayList<>(); @@ -366,13 +366,28 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); + // schedule 1st period 1st day + ProgressiveLoanInterestRepaymentModel repaymentDetails = emiCalculator + .getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 1)).get(); + Assertions.assertEquals(100, toDouble(repaymentDetails.getOutstandingBalance().getAmount())); + Assertions.assertEquals(82.99, toDouble(repaymentDetails.getRemainingBalance().getAmount())); + Assertions.assertEquals(17.01, toDouble(repaymentDetails.getPrincipalDue().getAmount())); + Assertions.assertEquals(0.0, toDouble(repaymentDetails.getInterestDue().getAmount())); + + // schedule 2nd period last day + repaymentDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)).get(); + Assertions.assertEquals(83.57, toDouble(repaymentDetails.getOutstandingBalance().getAmount())); + Assertions.assertEquals(16.52, toDouble(repaymentDetails.getPrincipalDue().getAmount())); + Assertions.assertEquals(0.49, toDouble(repaymentDetails.getInterestDue().getAmount())); + // partially pay off a period with balance correction + final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1); 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(); + .getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate, 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())); @@ -389,12 +404,13 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay checkPeriod(interestSchedule, 5, 0, 16.75, 0.005833333333, 0.10, 16.65, 0.0); // totally pay off another period with balance correction + final LocalDate op2ndCorrectionPeriodDueDate = LocalDate.of(2024, 4, 1); 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(); + .getPayableDetails(interestSchedule, op2ndCorrectionPeriodDueDate, 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())); @@ -410,18 +426,116 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay checkPeriod(interestSchedule, 5, 0, 16.34, 0.005833333333, 0.09, 16.25, 0.0); // check numbers on last period due date + LocalDate periodDueDate = LocalDate.of(2024, 7, 1); + LocalDate payDate = LocalDate.of(2024, 7, 1); final ProgressiveLoanInterestRepaymentModel repaymentDetails3rd = emiCalculator - .getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 1)).get(); + .getPayableDetails(interestSchedule, periodDueDate, payDate).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 + periodDueDate = LocalDate.of(2024, 7, 1); + payDate = LocalDate.of(2024, 7, 15); final ProgressiveLoanInterestRepaymentModel repaymentDetails4th = emiCalculator - .getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 15)).get(); + .getPayableDetails(interestSchedule, periodDueDate, payDate).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())); + + // balance update on the last period, check the right interest interval split + emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 6, 10), Money.of(monetaryCurrency, BigDecimal.ZERO)); + final var lastRepaymentPeriod = interestSchedule.repayments().get(interestSchedule.repayments().size() - 1); + Assertions.assertTrue(lastRepaymentPeriod.isLastPeriod()); + Assertions.assertEquals(2, lastRepaymentPeriod.getInterestPeriods().size()); + Assertions.assertEquals(LocalDate.of(2024, 6, 1), lastRepaymentPeriod.getInterestPeriods().getFirst().getFromDate()); + Assertions.assertEquals(LocalDate.of(2024, 6, 10), lastRepaymentPeriod.getInterestPeriods().getFirst().getDueDate()); + Assertions.assertEquals(LocalDate.of(2024, 6, 10), lastRepaymentPeriod.getInterestPeriods().getLast().getFromDate()); + Assertions.assertEquals(LocalDate.of(2024, 7, 1), lastRepaymentPeriod.getInterestPeriods().getLast().getDueDate()); + } + + @Test + public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_payoff_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 op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1); + 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, op1stCorrectionPeriodDueDate, 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())); + + ProgressiveLoanInterestRepaymentModel details = null; + // check getPayableDetails forcast + details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)).get(); + Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance().getAmount())); + Assertions.assertEquals(83.57, toDouble(details.getCorrectedOutstandingBalance().getAmount())); + Assertions.assertEquals(16.52, toDouble(details.getPrincipalDue().getAmount())); + Assertions.assertEquals(0.49, toDouble(details.getInterestDue().getAmount())); + + // apply balance change and check again + emiCalculator.addBalanceCorrection(interestSchedule, op1stCorrectionDate, op1stCorrectionAmount); + details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)).get(); + Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance().getAmount())); + Assertions.assertEquals(0, toDouble(details.getCorrectedOutstandingBalance().getAmount())); + Assertions.assertEquals(16.77, toDouble(details.getPrincipalDue().getAmount())); + Assertions.assertEquals(0.24, toDouble(details.getInterestDue().getAmount())); + + emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 3, 1), + Money.of(monetaryCurrency, BigDecimal.valueOf(-66.80))); + emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 4, 1), + Money.of(monetaryCurrency, BigDecimal.valueOf(-49.79))); + emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 5, 1), + Money.of(monetaryCurrency, BigDecimal.valueOf(-32.78))); + emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 6, 1), + Money.of(monetaryCurrency, BigDecimal.valueOf(-15.77))); + + details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1)).get(); + Assertions.assertEquals(15.77, toDouble(details.getOutstandingBalance().getAmount())); + Assertions.assertEquals(0, toDouble(details.getCorrectedOutstandingBalance().getAmount())); + Assertions.assertEquals(15.77, toDouble(details.getPrincipalDue().getAmount())); + Assertions.assertEquals(0.0, toDouble(details.getInterestDue().getAmount())); + + // check periods in model + 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, 17.01, 49.79); + checkPeriod(interestSchedule, 3, 0, 17.01, 0.005833333333, 0, 17.01, 32.78); + checkPeriod(interestSchedule, 4, 0, 17.01, 0.005833333333, 0, 17.01, 15.77); + checkPeriod(interestSchedule, 5, 0, 15.77, 0.005833333333, 0, 15.77, 0.0); } // @Test