diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java index aa1351880b3..265cc6a34fd 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/OutstandingAmountsDTO.java @@ -46,20 +46,24 @@ public Money getTotalOutstanding() { .plus(penaltyCharges()); } - public void plusPrincipal(Money principal) { + public OutstandingAmountsDTO plusPrincipal(Money principal) { this.principal = this.principal.plus(principal); + return this; } - public void plusInterest(Money interest) { + public OutstandingAmountsDTO plusInterest(Money interest) { this.interest = this.interest.plus(interest); + return this; } - public void plusFeeCharges(Money feeCharges) { + public OutstandingAmountsDTO plusFeeCharges(Money feeCharges) { this.feeCharges = this.feeCharges.plus(feeCharges); + return this; } - public void plusPenaltyCharges(Money penaltyCharges) { + public OutstandingAmountsDTO plusPenaltyCharges(Money penaltyCharges) { this.penaltyCharges = this.penaltyCharges.plus(penaltyCharges); + return this; } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 34d8baf6b5c..85091c5ff4d 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.portfolio.loanaccount.domain; +import static org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY; + import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; @@ -488,6 +490,10 @@ public void resetChargesCharged() { this.penaltyCharges = null; } + public boolean isCurrentInstallment(LocalDate transactionDate) { + return getFromDate().isBefore(transactionDate) && !getDueDate().isBefore(transactionDate); + } + public interface PaymentFunction { Money accept(LocalDate transactionDate, Money transactionAmountRemaining); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index a9b5773b94e..042518bac71 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -79,6 +79,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ProgressiveLoanScheduleGenerator; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; import org.apache.fineract.portfolio.loanproduct.domain.AllocationType; import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; @@ -88,6 +89,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.apache.commons.lang3.tuple.Pair; @Slf4j @RequiredArgsConstructor @@ -143,12 +145,13 @@ public Money handleRepaymentSchedule(List transactionsPostDisbu throw new NotImplementedException(); } - @Override - public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, - MonetaryCurrency currency, List installments, Set charges) { + // only for progressive loans + public Pair reprocessProgressiveLoanTransactions( + LocalDate disbursementDate, List loanTransactions, MonetaryCurrency currency, + List installments, Set charges) { final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); if (loanTransactions.isEmpty()) { - return changedTransactionDetail; + return Pair.of(changedTransactionDetail, null); } if (charges != null) { for (final LoanCharge loanCharge : charges) { @@ -185,10 +188,18 @@ public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursement chargeOrTransaction.getLoanCharge() .ifPresent(loanCharge -> processSingleCharge(loanCharge, currency, installments, disbursementDate)); } - List txs = chargeOrTransactions.stream().map(ChargeOrTransaction::getLoanTransaction).filter(Optional::isPresent) + List txs = chargeOrTransactions.stream() // + .map(ChargeOrTransaction::getLoanTransaction) // + .filter(Optional::isPresent) // .map(Optional::get).toList(); reprocessInstallments(disbursementDate, txs, installments, currency); - return changedTransactionDetail; + return Pair.of(changedTransactionDetail, scheduleModel); + } + + @Override + public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, + MonetaryCurrency currency, List installments, Set charges) { + return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft(); } @Override @@ -281,7 +292,7 @@ protected LoanTransaction findOriginalTransaction(LoanTransaction loanTransactio } return originalTransaction.get(); } else { // when there is no id, then it might be that the original transaction is changed, so we need to look - // it up from the Ctx. + // it up from the Ctx. Long originalChargebackTransactionId = ctx.getChangedTransactionDetail().getCurrentTransactionToOldId().get(loanTransaction); Collection updatedTransactions = ctx.getChangedTransactionDetail().getNewTransactionMappings().values(); Optional updatedTransaction = updatedTransactions.stream().filter(tr -> tr.getLoanTransactionRelations() @@ -982,6 +993,8 @@ private Money refundTransactionHorizontally(LoanTransaction loanTransaction, Mon break outerLoop; } } + default -> { + } } } } while (installments.stream().anyMatch(installment -> installment.getTotalPaid(currency).isGreaterThan(zero)) @@ -1157,22 +1170,22 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo // For having similar logic we are populating installment list even when the future installment // allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element. - List inAdvanceInstallments = new ArrayList<>(); - if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) { - inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) + List inAdvanceInstallments = switch (futureInstallmentAllocationRule) { + case REAMORTIZATION -> installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) .filter(e -> loanTransaction.isBefore(e.getDueDate())).toList(); - } else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) { - inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) - .filter(e -> loanTransaction.isBefore(e.getDueDate())) - .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); - } else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) { - inAdvanceInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) - .filter(e -> loanTransaction.isBefore(e.getDueDate())) - .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); - } + case NEXT_INSTALLMENT -> // first future unpaid installment + installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) + .filter(e -> loanTransaction.isBefore(e.getDueDate())) + .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); + case LAST_INSTALLMENT -> // last future unpaid installment + installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) + .filter(e -> loanTransaction.isBefore(e.getDueDate())) + .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); + }; int firstNormalInstallmentNumber = LoanRepaymentScheduleProcessingWrapper.fetchFirstNormalInstallmentNumber(installments); + Money originalInterestOfCurrentInstallment = null; for (PaymentAllocationType paymentAllocationType : paymentAllocationTypes) { switch (paymentAllocationType.getDueType()) { case PAST_DUE -> { @@ -1206,6 +1219,53 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo case IN_ADVANCE -> { int numberOfInstallments = inAdvanceInstallments.size(); if (numberOfInstallments > 0) { + if (loanTransaction.getLoan().getLoanProduct().isInterestRecalculationEnabled()) { + AtomicReference sumAdjusted = new AtomicReference<>(Money.zero(currency)); + + // recalculate interest before processing payment + for (LoanRepaymentScheduleInstallment installment : inAdvanceInstallments) { + LocalDate transactionDate = loanTransaction.getTransactionDate(); + if (installment.isCurrentInstallment(transactionDate)) { + switch (paymentAllocationType) { + case IN_ADVANCE_INTEREST -> { + Money payableInterest = ProgressiveLoanScheduleGenerator + .calculatePayableInterest(installment, transactionDate); + originalInterestOfCurrentInstallment = installment.getInterestCharged(currency); + installment.updateInterestCharged(payableInterest.getAmount()); + } + case IN_ADVANCE_PRINCIPAL -> { + Money interestDelta = calculateInterestDelta(originalInterestOfCurrentInstallment, + installment, transactionDate, currency); + sumAdjusted.updateAndGet(v -> v.add(interestDelta)); + BigDecimal newPrincipal = installment.getPrincipal(currency).plus(interestDelta) + .getAmount(); + installment.updatePrincipal(newPrincipal); + } + default -> { + } + } + + int lastInstallmentNumber = installments.stream() // + .mapToInt(LoanRepaymentScheduleInstallment::getInstallmentNumber) // + .max().orElse(0); + + // update later installments with zero interest and increased principal + installments.stream().filter(it -> it.getInstallmentNumber() > installment.getInstallmentNumber()) + .forEach(it -> { + Money interestCharged = it.getInterestCharged(currency); + sumAdjusted.updateAndGet(v -> v.add(interestCharged)); + it.updateInterestCharged(BigDecimal.ZERO); + BigDecimal newPrincipal = it.getPrincipal(currency).plus(interestCharged).getAmount(); + if (it.getInstallmentNumber() == lastInstallmentNumber) { + // adjust last installment to match the outstanding balance + newPrincipal = newPrincipal.subtract(sumAdjusted.get().getAmount()); + } + it.updatePrincipal(newPrincipal); + }); + } + } + } + // This will be the same amount as transactionAmountUnprocessed in case of the future // installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode()); @@ -1239,6 +1299,19 @@ private Money processAllocationsHorizontally(LoanTransaction loanTransaction, Mo return transactionAmountUnprocessed; } + private Money calculateInterestDelta(Money originalInterestOfCurrentInstallment, LoanRepaymentScheduleInstallment installment, + LocalDate transactionDate, MonetaryCurrency currency) { + if (originalInterestOfCurrentInstallment == null) { + // interest was not recalculated yet + Money payableInterest = ProgressiveLoanScheduleGenerator.calculatePayableInterest(installment, transactionDate); + originalInterestOfCurrentInstallment = installment.getInterestCharged(currency); + return originalInterestOfCurrentInstallment.minus(payableInterest); + } else { + // interest was already recalculated + return originalInterestOfCurrentInstallment.minus(installment.getInterestCharged(currency)); + } + } + @NotNull private static Set getLoanChargesOfInstallment(Set charges, LoanRepaymentScheduleInstallment currentInstallment, int firstNormalInstallmentNumber) { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java index adfd1f1b78d..972358aaf17 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -19,6 +19,7 @@ package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; import static java.time.temporal.ChronoUnit.DAYS; +import static org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE; import java.math.BigDecimal; import java.math.MathContext; @@ -28,8 +29,8 @@ import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; -import java.util.concurrent.atomic.AtomicBoolean; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.infrastructure.core.service.MathUtil; @@ -45,6 +46,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams; @@ -241,58 +243,37 @@ public LoanScheduleDTO rescheduleNextInstallments(MathContext mc, LoanApplicatio public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency, LocalDate onDate, LoanApplicationTerms loanApplicationTerms, MathContext mc, Loan loan, HolidayDetailDTO holidayDetailDTO, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { - return switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { - case TILL_PRE_CLOSURE_DATE -> { - log.debug("calculating prepayment amount till pre closure date (Strategy A)"); - OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency); - AtomicBoolean firstAfterPayoff = new AtomicBoolean(true); - loan.getRepaymentScheduleInstallments().forEach(installment -> { - boolean isInstallmentAfterPayoff = installment.getDueDate().isAfter(onDate); - - outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency)); - if (isInstallmentAfterPayoff) { - if (firstAfterPayoff.getAndSet(false)) { - outstandingAmounts.plusInterest(calculatePayableInterest(loan, installment, onDate)); - } else { - log.debug("Installment {} - {} is after payoff, not counting interest", installment.getFromDate(), - installment.getDueDate()); - } - } else { - log.debug("adding interest for {} - {}: {}", installment.getFromDate(), installment.getDueDate(), - installment.getInterestOutstanding(currency)); - outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency)); - } - outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency)); - outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)); - }); - yield outstandingAmounts; - } - - case TILL_REST_FREQUENCY_DATE -> { - log.debug("calculating prepayment amount till rest frequency date (Strategy B)"); - OutstandingAmountsDTO outstandingAmounts = new OutstandingAmountsDTO(currency); - loan.getRepaymentScheduleInstallments().forEach(installment -> { - boolean isPayoffBeforeInstallment = installment.getFromDate().isBefore(onDate); - - outstandingAmounts.plusPrincipal(installment.getPrincipalOutstanding(currency)); - if (isPayoffBeforeInstallment) { - outstandingAmounts.plusInterest(installment.getInterestOutstanding(currency)); - } else { - log.debug("Payoff after installment {}, not counting interest", installment.getDueDate()); - } - outstandingAmounts.plusFeeCharges(installment.getFeeChargesOutstanding(currency)); - outstandingAmounts.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)); - }); - - yield outstandingAmounts; - } - case NONE -> throw new UnsupportedOperationException("Pre-closure interest calculation strategy not supported"); + List installments = loan.getRepaymentScheduleInstallments(); + + LocalDate transactionDate = switch (loanApplicationTerms.getPreClosureInterestCalculationStrategy()) { + case TILL_PRE_CLOSURE_DATE -> onDate; + case TILL_REST_FREQUENCY_DATE -> // find due date of current installment + installments.stream().filter(it -> it.getFromDate().isBefore(onDate) && it.getDueDate().isAfter(onDate)).findFirst() + .orElseThrow(() -> new IllegalStateException("No installment found for transaction date: " + onDate)).getDueDate(); + case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE"); }; + + if (!(loanRepaymentScheduleTransactionProcessor instanceof AdvancedPaymentScheduleTransactionProcessor processor)) { + throw new IllegalStateException("Expected an AdvancedPaymentScheduleTransactionProcessor"); + } + ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), + loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight(); + ProgressiveLoanInterestRepaymentModel result = emiCalculator.getPayableDetails(model, transactionDate).orElseThrow(); + + OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) // + .principal(result.getOutstandingBalance()) // + .interest(result.getInterestDue()); + + installments.forEach(installment -> amounts // + .plusFeeCharges(installment.getFeeChargesOutstanding(currency)) + .plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency))); + + return amounts; } - private Money calculatePayableInterest(Loan loan, LoanRepaymentScheduleInstallment installment, LocalDate onDate) { + public static Money calculatePayableInterest(LoanRepaymentScheduleInstallment installment, LocalDate onDate) { RoundingMode roundingMode = MoneyHelper.getRoundingMode(); - MonetaryCurrency currency = loan.getCurrency(); + MonetaryCurrency currency = installment.getLoan().getCurrency(); Money originalInterest = installment.getInterestCharged(currency); log.debug("calculating interest for {} from {} to {}", originalInterest, installment.getFromDate(), installment.getDueDate()); 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..795d0e679f3 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 @@ -28,6 +28,7 @@ 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.loanaccount.loanschedule.domain.PrincipalInterest; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; public interface EMICalculator { 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..a177b84343f 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 @@ -32,6 +32,7 @@ import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.common.domain.DaysInMonthType; import org.apache.fineract.portfolio.common.domain.DaysInYearType; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; @@ -40,6 +41,7 @@ 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.loanaccount.loanschedule.domain.PrincipalInterest; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.mapper.ProgressiveLoanInterestRepaymentModelMapper; import org.jetbrains.annotations.NotNull; diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java index 214bff051c9..945d370e6c6 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGeneratorTest.java @@ -121,7 +121,7 @@ public void calculatePrepaymentAmount_TILL_PRE_CLOSURE_DATE() { OutstandingAmountsDTO amounts = generator.calculatePrepaymentAmount(usd, LocalDate.of(2024, 2, 15), terms, MathContext.DECIMAL32, loan, holidays, processor); - assertEquals(BigDecimal.valueOf(83.84), amounts.getTotalOutstanding().getAmount()); + assertEquals(BigDecimal.valueOf(83.81), amounts.getTotalOutstanding().getAmount()); } @Test