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 429d1b18dd8..fde017758bb 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 @@ -59,7 +59,6 @@ import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.MathUtil; -import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; @@ -82,7 +81,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; @@ -149,7 +148,7 @@ public Money handleRepaymentSchedule(List transactionsPostDisbu // only for progressive loans public Pair reprocessProgressiveLoanTransactions( - LocalDate disbursementDate, List loanTransactions, MonetaryCurrency currency, + LocalDate disbursementDate, LocalDate currentDate, List loanTransactions, MonetaryCurrency currency, List installments, Set charges) { final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); if (loanTransactions.isEmpty()) { @@ -213,7 +212,7 @@ public Pair repr LoanTransaction newTransaction = newTransactionMappings.get(oldTransactionId); createNewTransaction(oldTransaction, newTransaction, ctx); } - recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx); + recalculateInterestForDate(currentDate, ctx); List txs = changeOperations.stream() // .filter(ChangeOperation::isTransaction) // .map(e -> e.getLoanTransaction().get()).toList(); @@ -248,7 +247,9 @@ private void updateInstallmentIfInterestPeriodPresent(final ProgressiveLoanInter @Override public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, MonetaryCurrency currency, List installments, Set charges) { - return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft(); + LocalDate currentDate = DateUtils.getBusinessLocalDate(); + return reprocessProgressiveLoanTransactions(disbursementDate, currentDate, loanTransactions, currency, installments, charges) + .getLeft(); } @NotNull @@ -1590,11 +1591,11 @@ private void updateRepaymentPeriods(LoanTransaction loanTransaction, Progressive private void updateRepaymentPeriodBalances(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment inAdvanceInstallment, ProgressiveLoanInterestScheduleModel model, LocalDate payDate) { - PayableDetails payableDetails = emiCalculator.getPayableDetails(model, inAdvanceInstallment.getDueDate(), payDate); + PeriodDueDetails payableDetails = emiCalculator.getDueAmounts(model, inAdvanceInstallment.getDueDate(), payDate); switch (paymentAllocationType) { - case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getPayableInterest().getAmount()); - case IN_ADVANCE_PRINCIPAL -> inAdvanceInstallment.updatePrincipal(payableDetails.getPayablePrincipal().getAmount()); + case IN_ADVANCE_INTEREST -> inAdvanceInstallment.updateInterestCharged(payableDetails.getDueInterest().getAmount()); + case IN_ADVANCE_PRINCIPAL -> inAdvanceInstallment.updatePrincipal(payableDetails.getDuePrincipal().getAmount()); default -> { } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/OutstandingDetails.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/OutstandingDetails.java new file mode 100644 index 00000000000..7f9d722fe01 --- /dev/null +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/OutstandingDetails.java @@ -0,0 +1,29 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.loanschedule.data; + +import lombok.Data; +import org.apache.fineract.organisation.monetary.domain.Money; + +@Data +public class OutstandingDetails { + + private final Money outstandingPrincipal; + private final Money outstandingInterest; +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PayableDetails.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PeriodDueDetails.java similarity index 86% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PayableDetails.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PeriodDueDetails.java index dd131e205a3..ce9c8fe796e 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PayableDetails.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/PeriodDueDetails.java @@ -22,10 +22,9 @@ import org.apache.fineract.organisation.monetary.domain.Money; @Data -public class PayableDetails { +public class PeriodDueDetails { private final Money emi; - private final Money payablePrincipal; - private final Money payableInterest; - private final Money outstandingBalance; + private final Money duePrincipal; + private final Money dueInterest; } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java index 5451886da2b..ad7acca6fa4 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java @@ -197,4 +197,21 @@ public Money getZero() { return Money.zero(loanProductRelatedDetail.getCurrency(), mc); } + public Money getTotalDueInterest() { + return repaymentPeriods().stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getCalculatedDueInterest)) + .reduce(getZero(), Money::plus); + } + + public Money getTotalDuePrincipal() { + return repaymentPeriods.stream().flatMap(rp -> rp.getInterestPeriods().stream().map(InterestPeriod::getDisbursementAmount)) + .reduce(getZero(), Money::plus); + } + + public Money getTotalPaidInterest() { + return repaymentPeriods().stream().map(RepaymentPeriod::getPaidInterest).reduce(getZero(), Money::plus); + } + + public Money getTotalPaidPrincipal() { + return repaymentPeriods().stream().map(RepaymentPeriod::getPaidPrincipal).reduce(getZero(), Money::plus); + } } 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 58e16fc1edd..c11ee1fa98a 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 @@ -46,7 +46,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleModelDownPaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleParams; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.exception.MultiDisbursementOutstandingAmoutException; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; @@ -195,7 +195,8 @@ private void processDisbursements(final LoanApplicationTerms loanApplicationTerm continue; } - Money outstandingBalance = emiCalculator.getOutstandingLoanBalance(interestScheduleModel, periodDueDate, disbursementDate); + Money outstandingBalance = emiCalculator.getOutstandingLoanBalanceOfPeriod(interestScheduleModel, periodDueDate, + disbursementDate); final Money disbursedAmount = Money.of(loanApplicationTerms.getCurrency(), disbursementData.getPrincipal(), mc); final LoanScheduleModelDisbursementPeriod disbursementPeriod = LoanScheduleModelDisbursementPeriod @@ -263,17 +264,14 @@ public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency case NONE -> throw new IllegalStateException("Unexpected PreClosureInterestCalculationStrategy: NONE"); }; - ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), + ProgressiveLoanInterestScheduleModel model = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), onDate, loan.retrieveListOfTransactionsForReprocessing(), currency, installments, loan.getActiveCharges()).getRight(); - PayableDetails result = emiCalculator.getPayableDetails(model, actualInstallment.getDueDate(), transactionDate); + OutstandingDetails result = emiCalculator.getOutstandingAmountsTillDate(model, transactionDate); // TODO: We should add all the past due outstanding amounts as well OutstandingAmountsDTO amounts = new OutstandingAmountsDTO(currency) // - .principal(result.getOutstandingBalance()) // - .interest(result.getPayableInterest()); - - installments.stream().filter(installment -> installment.getDueDate().isBefore(onDate)) - .forEach(installment -> amounts.plusInterest(installment.getInterestOutstanding(currency))); + .principal(result.getOutstandingPrincipal()) // + .interest(result.getOutstandingInterest());// installments.forEach(installment -> amounts // .plusFeeCharges(installment.getFeeChargesOutstanding(currency)) 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 ac10aeab3e4..0f9221174ca 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 @@ -25,7 +25,8 @@ import java.util.Optional; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; @@ -55,8 +56,10 @@ void payInterest(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate r void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, LocalDate transactionDate, Money principalAmount); - PayableDetails getPayableDetails(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate payDate); + PeriodDueDetails getDueAmounts(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate periodDueDate, LocalDate targetDate); - Money getOutstandingLoanBalance(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate, + Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate, LocalDate targetDate); + + OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel model, LocalDate targetDate); } 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 8ccda6535e7..a0f57dfb6d3 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 @@ -35,11 +35,13 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OutstandingDetails; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; +import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; @Component @@ -156,8 +158,60 @@ public void payPrincipal(ProgressiveLoanInterestScheduleModel scheduleModel, Loc } @Override - public PayableDetails getPayableDetails(final ProgressiveLoanInterestScheduleModel scheduleModel, - final LocalDate repaymentPeriodDueDate, final LocalDate targetDate) { + public PeriodDueDetails getDueAmounts(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate, + final LocalDate targetDate) { + ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel, + repaymentPeriodDueDate, targetDate); + RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow(); + boolean multiplePeriodIsUnpaid = recalculatedScheduleModelTillDate.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()) + .count() > 1L; + if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) { + repaymentPeriod.setEmi(repaymentPeriod.getOriginalEmi()); + } + + return new PeriodDueDetails(repaymentPeriod.getEmi(), // + repaymentPeriod.getDuePrincipal(), // + repaymentPeriod.getDueInterest()); // + } + + @Override + public Money getOutstandingLoanBalanceOfPeriod(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate repaymentPeriodDueDate, + LocalDate targetDate) { + ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate = recalculateScheduleModelTillDate(scheduleModel, + repaymentPeriodDueDate, targetDate); + RepaymentPeriod repaymentPeriod = recalculatedScheduleModelTillDate.findRepaymentPeriod(repaymentPeriodDueDate).orElseThrow(); + + return repaymentPeriod.getOutstandingLoanBalance(); + } + + @Override + public OutstandingDetails getOutstandingAmountsTillDate(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate targetDate) { + MathContext mc = scheduleModel.mc(); + ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc); + + scheduleModelCopy.repaymentPeriods().stream()// + .filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()// + .flatMap(rp -> rp.getInterestPeriods().stream()// + .filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) // + .reduce((one, two) -> two)) + .ifPresent(ip -> ip.setDueDate(targetDate)); // + + calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy); + scheduleModelCopy.repaymentPeriods().forEach(rp -> rp.getInterestPeriods().stream() + .filter(ip -> targetDate.isBefore(ip.getDueDate())).forEach(ip -> ip.setRateFactor(BigDecimal.ZERO))); + calculateOutstandingBalance(scheduleModelCopy); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy); + + Money totalOutstandingPrincipal = MathUtil + .negativeToZero(scheduleModelCopy.getTotalDuePrincipal().minus(scheduleModelCopy.getTotalPaidPrincipal())); + Money totalOutstandingInterest = MathUtil + .negativeToZero(scheduleModelCopy.getTotalDueInterest().minus(scheduleModelCopy.getTotalPaidInterest())); + return new OutstandingDetails(totalOutstandingPrincipal, totalOutstandingInterest); + } + + @NotNull + private ProgressiveLoanInterestScheduleModel recalculateScheduleModelTillDate(ProgressiveLoanInterestScheduleModel scheduleModel, + LocalDate repaymentPeriodDueDate, LocalDate targetDate) { MathContext mc = scheduleModel.mc(); ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc); RepaymentPeriod repaymentPeriod = scheduleModelCopy.repaymentPeriods().stream() @@ -175,11 +229,12 @@ public PayableDetails getPayableDetails(final ProgressiveLoanInterestScheduleMod interestPeriod = repaymentPeriod.getInterestPeriods().stream() .filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())).findFirst().orElseThrow(); } - scheduleModelCopy.repaymentPeriods().stream() - .filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst() - .ifPresent(rp -> rp.getInterestPeriods().stream() - .filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) - .reduce((one, two) -> two).ifPresent(ip -> ip.setDueDate(targetDate))); + scheduleModelCopy.repaymentPeriods().stream()// + .filter(rp -> targetDate.isAfter(rp.getFromDate()) && !targetDate.isAfter(rp.getDueDate())).findFirst()// + .flatMap(rp -> rp.getInterestPeriods().stream()// + .filter(ip -> targetDate.isAfter(ip.getFromDate()) && !targetDate.isAfter(ip.getDueDate())) // + .reduce((one, two) -> two)) + .ifPresent(ip -> ip.setDueDate(targetDate)); // interestPeriod.setDueDate(adjustedTargetDate); int index = repaymentPeriod.getInterestPeriods().indexOf(interestPeriod); repaymentPeriod.getInterestPeriods().subList(index + 1, repaymentPeriod.getInterestPeriods().size()).clear(); @@ -188,19 +243,7 @@ public PayableDetails getPayableDetails(final ProgressiveLoanInterestScheduleMod calculateOutstandingBalance(scheduleModelCopy); calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy); - boolean multiplePeriodIsUnpaid = scheduleModelCopy.repaymentPeriods().stream().filter(rp -> !rp.isFullyPaid()).count() > 1L; - if (multiplePeriodIsUnpaid && !targetDate.isAfter(repaymentPeriod.getFromDate())) { - repaymentPeriod.setEmi(repaymentPeriod.getOriginalEmi()); - } - - return new PayableDetails(repaymentPeriod.getEmi(), repaymentPeriod.getDuePrincipal(), repaymentPeriod.getDueInterest(), - interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount(), mc)); - } - - @Override - public Money getOutstandingLoanBalance(ProgressiveLoanInterestScheduleModel interestScheduleModel, LocalDate repaymentPeriodDueDate, - LocalDate targetDate) { - return getPayableDetails(interestScheduleModel, repaymentPeriodDueDate, targetDate).getOutstandingBalance(); + return scheduleModelCopy; } /** diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrepaymentCalculationTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrepaymentCalculationTest.java deleted file mode 100644 index 1fec1bb9262..00000000000 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrepaymentCalculationTest.java +++ /dev/null @@ -1,146 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.loanaccount.loanschedule.domain; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.mockito.Mockito.when; - -import java.math.BigDecimal; -import java.math.MathContext; -import java.math.RoundingMode; -import java.time.LocalDate; -import java.util.List; -import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; -import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; -import org.apache.fineract.organisation.monetary.domain.Money; -import org.apache.fineract.organisation.monetary.domain.MoneyHelper; -import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; -import org.apache.fineract.portfolio.loanaccount.domain.Loan; -import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; -import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; -import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod; -import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; -import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy; -import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.MockedStatic; -import org.mockito.Mockito; -import org.mockito.MockitoAnnotations; - -public class PrepaymentCalculationTest { - - private static final MockedStatic moneyHelper = Mockito.mockStatic(MoneyHelper.class); - private static final MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN); - private static final MonetaryCurrency monetaryCurrency = MonetaryCurrency - .fromApplicationCurrency(new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$")); - - @Mock - private LoanProductRelatedDetail loanProductRelatedDetail; - - @Mock - private LoanApplicationTerms loanApplicationTerms; - - @Mock - private AdvancedPaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor; - - @Mock - private EMICalculator emiCalculator; - - @InjectMocks - private ProgressiveLoanScheduleGenerator progressiveLoanScheduleGenerator; - - private Loan loan; - - @BeforeEach - public void setUp() { - MockitoAnnotations.openMocks(this); - - moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP)); - moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP); - - loan = Mockito.mock(Loan.class); - when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE); - when(loan.getDisbursementDate()).thenReturn(LocalDate.of(2022, 9, 7)); - when(loan.getRepaymentScheduleInstallments()).thenReturn(createRepaymentScheduleInstallments()); - - when(loanApplicationTerms.getPreClosureInterestCalculationStrategy()) - .thenReturn(LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE); - - when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(BigDecimal.valueOf(7.0)); - - List repaymentPeriods = createMockRepaymentPeriods(); - ProgressiveLoanInterestScheduleModel scheduleModel = new ProgressiveLoanInterestScheduleModel(repaymentPeriods, - loanProductRelatedDetail, 100, mc); - - when(loanRepaymentScheduleTransactionProcessor.reprocessProgressiveLoanTransactions(Mockito.any(), Mockito.anyList(), Mockito.any(), - Mockito.anyList(), Mockito.anySet())).thenReturn(org.apache.commons.lang3.tuple.Pair.of(null, scheduleModel)); - - PayableDetails payableDetails = new PayableDetails(Money.of(monetaryCurrency, BigDecimal.valueOf(200)), - Money.of(monetaryCurrency, BigDecimal.valueOf(500)), Money.of(monetaryCurrency, BigDecimal.valueOf(0)), - Money.of(monetaryCurrency, BigDecimal.valueOf(1000))); - - when(emiCalculator.getPayableDetails(Mockito.any(ProgressiveLoanInterestScheduleModel.class), Mockito.any(LocalDate.class), - Mockito.any(LocalDate.class))).thenReturn(payableDetails); - } - - @AfterAll - public static void tearDown() { - moneyHelper.close(); - } - - @Test - public void testCalculatePrepaymentAmount() { - LocalDate prepaymentDate = LocalDate.of(2023, 6, 1); - - OutstandingAmountsDTO result = progressiveLoanScheduleGenerator.calculatePrepaymentAmount(monetaryCurrency, prepaymentDate, - loanApplicationTerms, mc, loan, null, loanRepaymentScheduleTransactionProcessor); - - assertEquals("1000.00", result.principal().getAmount().toString()); - assertEquals("15.00", result.interest().getAmount().toString()); - } - - private List createRepaymentScheduleInstallments() { - LoanRepaymentScheduleInstallment installment1 = new LoanRepaymentScheduleInstallment(loan, 1, LocalDate.of(2022, 10, 1), - LocalDate.of(2022, 11, 1), BigDecimal.valueOf(500), BigDecimal.valueOf(10), BigDecimal.ZERO, BigDecimal.ZERO, false, null); - - LoanRepaymentScheduleInstallment installment2 = new LoanRepaymentScheduleInstallment(loan, 2, LocalDate.of(2022, 11, 2), - LocalDate.of(2022, 12, 1), BigDecimal.valueOf(500), BigDecimal.valueOf(5), BigDecimal.ZERO, BigDecimal.ZERO, false, null); - - return List.of(installment1, installment2); - } - - private List createMockRepaymentPeriods() { - RepaymentPeriod period1 = Mockito.mock(RepaymentPeriod.class); - when(period1.getFromDate()).thenReturn(LocalDate.of(2022, 10, 1)); - when(period1.getDueDate()).thenReturn(LocalDate.of(2022, 11, 1)); - - RepaymentPeriod period2 = Mockito.mock(RepaymentPeriod.class); - when(period2.getFromDate()).thenReturn(LocalDate.of(2022, 11, 2)); - when(period2.getDueDate()).thenReturn(LocalDate.of(2022, 12, 1)); - - return List.of(period1, period2); - } -} 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 a2bd699ba3f..32b6bfe2d12 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 @@ -35,7 +35,7 @@ import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.InterestPeriod; -import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PeriodDueDetails; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelRepaymentPeriod; @@ -558,17 +558,14 @@ public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInM emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); // schedule 1st period 1st day - PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), - LocalDate.of(2024, 1, 1)); - Assertions.assertEquals(100, toDouble(payableDetails.getOutstandingBalance())); - Assertions.assertEquals(17.01, toDouble(payableDetails.getPayablePrincipal())); - Assertions.assertEquals(0.0, toDouble(payableDetails.getPayableInterest())); + PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 1)); + Assertions.assertEquals(17.01, toDouble(dueAmounts.getDuePrincipal())); + Assertions.assertEquals(0.0, toDouble(dueAmounts.getDueInterest())); // schedule 2nd period last day - payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); - Assertions.assertEquals(83.57, toDouble(payableDetails.getOutstandingBalance())); - Assertions.assertEquals(16.52, toDouble(payableDetails.getPayablePrincipal())); - Assertions.assertEquals(0.49, toDouble(payableDetails.getPayableInterest())); + dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); + Assertions.assertEquals(16.52, toDouble(dueAmounts.getDuePrincipal())); + Assertions.assertEquals(0.49, toDouble(dueAmounts.getDueInterest())); // pay off a period with balance correction final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1); @@ -576,11 +573,10 @@ public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInM final Money op1stCorrectionAmount = toMoney(16.77); // get remaining balance and dues for a date - final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate, + final PeriodDueDetails repaymentDetails1st = emiCalculator.getDueAmounts(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate); - Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance())); - Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal())); - Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest())); + Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getDuePrincipal())); + Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getDueInterest())); emiCalculator.payPrincipal(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, op1stCorrectionAmount); emiCalculator.payInterest(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, toMoney(0.24)); @@ -599,11 +595,10 @@ public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInM final Money op2ndCorrectionAmount = toMoney(16.42); // get remaining balance and dues for a date - final PayableDetails repaymentDetails2st = emiCalculator.getPayableDetails(interestSchedule, op2ndCorrectionPeriodDueDate, + final PeriodDueDetails repaymentDetails2st = emiCalculator.getDueAmounts(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate); - Assertions.assertEquals(66.80, toDouble(repaymentDetails2st.getOutstandingBalance())); - Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getPayablePrincipal())); - Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getPayableInterest())); + Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getDuePrincipal())); + Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getDueInterest())); emiCalculator.payPrincipal(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, op2ndCorrectionAmount); emiCalculator.payInterest(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, toMoney(0.49)); @@ -619,18 +614,16 @@ public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInM // check numbers on last period due date LocalDate periodDueDate = LocalDate.of(2024, 7, 1); LocalDate payDate = LocalDate.of(2024, 7, 1); - final PayableDetails repaymentDetails3rd = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate); - Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getOutstandingBalance())); - Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getPayablePrincipal())); - Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getPayableInterest())); + final PeriodDueDetails repaymentDetails3rd = emiCalculator.getDueAmounts(interestSchedule, periodDueDate, payDate); + Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getDuePrincipal())); + Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getDueInterest())); // check numbers after the last period due date periodDueDate = LocalDate.of(2024, 7, 1); payDate = LocalDate.of(2024, 7, 15); - final PayableDetails repaymentDetails4th = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate); - Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getOutstandingBalance())); - Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getPayablePrincipal())); - Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getPayableInterest())); + final PeriodDueDetails repaymentDetails4th = emiCalculator.getDueAmounts(interestSchedule, periodDueDate, payDate); + Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getDuePrincipal())); + Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getDueInterest())); // 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)); @@ -678,25 +671,22 @@ public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repay final Money op1stCorrectionAmount = toMoney(15.0); // get remaining balance and dues for a date - final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate, + final PeriodDueDetails repaymentDetails1st = emiCalculator.getDueAmounts(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate); - Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance())); - Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal())); - Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest())); + Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getDuePrincipal())); + Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getDueInterest())); - PayableDetails details = null; - // check getPayableDetails forcast - details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); - Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance())); - Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal())); - Assertions.assertEquals(0.49, toDouble(details.getPayableInterest())); + PeriodDueDetails details = null; + // check getdueAmounts forcast + details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); + Assertions.assertEquals(16.52, toDouble(details.getDuePrincipal())); + Assertions.assertEquals(0.49, toDouble(details.getDueInterest())); // apply balance change and check again emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), op1stCorrectionDate, op1stCorrectionAmount); - details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); - Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance())); - Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal())); - Assertions.assertEquals(0.49, toDouble(details.getPayableInterest())); + details = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); + Assertions.assertEquals(16.52, toDouble(details.getDuePrincipal())); + Assertions.assertEquals(0.49, toDouble(details.getDueInterest())); emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), toMoney(1.43)); emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), toMoney(0.58)); @@ -749,21 +739,20 @@ public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repay emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); // get remaining balance and dues on due date - PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), - LocalDate.of(2024, 2, 1)); - Assertions.assertEquals(16.43, toDouble(payableDetails.getPayablePrincipal())); - Assertions.assertEquals(0.58, toDouble(payableDetails.getPayableInterest())); + PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1)); + Assertions.assertEquals(16.43, toDouble(dueAmounts.getDuePrincipal())); + Assertions.assertEquals(0.58, toDouble(dueAmounts.getDueInterest())); // check numbers on payoff date - payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15)); - Assertions.assertEquals(16.75, toDouble(payableDetails.getPayablePrincipal())); - Assertions.assertEquals(0.26, toDouble(payableDetails.getPayableInterest())); + dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15)); + Assertions.assertEquals(16.75, toDouble(dueAmounts.getDuePrincipal())); + Assertions.assertEquals(0.26, toDouble(dueAmounts.getDueInterest())); emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(16.75)); emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(0.26)); // check again numbers are zero - // payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), + // dueAmounts = emiCalculator.getdueAmounts(interestSchedule, LocalDate.of(2024, 2, 1), // LocalDate.of(2024, 2, 1)); emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 1, 15), toMoney(17.01)); @@ -771,9 +760,9 @@ public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repay emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 1, 15), toMoney(17.01)); emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 1, 15), toMoney(17.01)); - payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1)); - Assertions.assertEquals(15.21, toDouble(payableDetails.getPayablePrincipal())); - Assertions.assertEquals(0.5, toDouble(payableDetails.getPayableInterest())); + dueAmounts = emiCalculator.getDueAmounts(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1)); + Assertions.assertEquals(15.21, toDouble(dueAmounts.getDuePrincipal())); + Assertions.assertEquals(0.5, toDouble(dueAmounts.getDueInterest())); // check periods in model checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.26, 16.75, 15.21); @@ -1261,9 +1250,9 @@ private static LoanRepaymentScheduleInstallment createPeriod(int periodId, Local private static void checkDailyInterest(final ProgressiveLoanInterestScheduleModel interestModel, final LocalDate repaymentPeriodDueDate, final LocalDate interestStartDay, final int dayOffset, final double dailyInterest, final double interest) { Money previousInterest = emiCalculator - .getPayableDetails(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset - 1)).getPayableInterest(); - Money currentInterest = emiCalculator.getPayableDetails(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset)) - .getPayableInterest(); + .getDueAmounts(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset - 1)).getDueInterest(); + Money currentInterest = emiCalculator.getDueAmounts(interestModel, repaymentPeriodDueDate, interestStartDay.plusDays(dayOffset)) + .getDueInterest(); Assertions.assertEquals(dailyInterest, toDouble(currentInterest.minus(previousInterest))); Assertions.assertEquals(interest, toDouble(currentInterest)); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java index f9ceda2e209..62ec46dcef5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanInterestRefundServiceImpl.java @@ -100,15 +100,15 @@ private BigDecimal totalInterest(final Loan loan, BigDecimal refundAmount, Local loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList()); Pair reprocessResult = processor - .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), transactionsToReprocess, loan.getCurrency(), - installmentsToReprocess, loan.getActiveCharges()); + .reprocessProgressiveLoanTransactions(loan.getDisbursementDate(), relatedRefundTransactionDate, transactionsToReprocess, + loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges()); loan.getLoanTransactions().addAll(reprocessResult.getLeft().getCurrentTransactionToOldId().keySet()); ProgressiveLoanInterestScheduleModel modelAfter = reprocessResult.getRight(); payableInterest = installmentsToReprocess.stream() // .map(installment -> emiCalculator // - .getPayableDetails(modelAfter, installment.getDueDate(), relatedRefundTransactionDate) // - .getPayableInterest() // + .getDueAmounts(modelAfter, installment.getDueDate(), relatedRefundTransactionDate) // + .getDueInterest() // .getAmount()) // .reduce(BigDecimal.ZERO, BigDecimal::add); // } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index 286b696807a..645ea67feb8 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -5518,6 +5518,197 @@ public void uc150() { }); } + // UC151: Validate Prepayment calculation till Pay-off date + // 1. Create a Loan product with Adv. Pment. Alloc. and with Declining Balance, 7% interest rate + // 2. Submit Loan, approve and Disburse + // 3. Validate Pay-off amounts on 31/12/2023, 01/01/2024, 02/01/2024, 01/02/2024, 02/02/2024, 15/02/2024, 01/03/2024 + // 4. Pay 1st installment fully on the due date of the 1st period + // 5. Validate Pay-off amounts on 31/12/2023, 01/01/2024, 02/01/2024, 01/02/2024, 02/02/2024, 15/02/2024, 01/03/2024 + // 6. Pay 2nd installment partially in the middle of 2nd period (15/02/2024) + // 7. Validate Pay-off amounts on 31/12/2023, 01/01/2024, 02/01/2024, 01/02/2024, 02/02/2024, 15/02/2024, 01/03/2024 + // 8. Prepay the loan on 15/02/2024 + @Test + public void uc151() { + String operationDate = "01 January 2024"; + AtomicLong createdLoanId = new AtomicLong(); + runAt(operationDate, () -> { + Long clientId = client.getClientId(); + PostLoanProductsRequest product = create4IProgressive().interestRateFrequencyType(YEARS).numberOfRepayments(6)// + .repaymentEvery(1)// + .repaymentFrequencyType(1L)// + .allowPartialPeriodInterestCalcualtion(false)// + .multiDisburseLoan(true)// + .maxTrancheCount(10)// + .disallowExpectedDisbursements(true)// + .allowApprovedDisbursedAmountsOverApplied(null)// + .overAppliedCalculationType(null)// + .overAppliedNumber(null)// + .installmentAmountInMultiplesOf(null)// + .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) // + ;// + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLP2ProgressiveLoanRequest(clientId, loanProductResponse.getResourceId(), + operationDate, 1000.0, 7.0, 6, null); + + applicationRequest = applicationRequest.interestCalculationPeriodType(DAYS) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + createdLoanId.set(loanResponse.getLoanId()); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN) + .approvedOnDate(operationDate).locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate) + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(100.0)).locale("en")); + + // Before 1st disbursement date + HashMap prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2023, 12, 31)); + assertEquals(100.0f, prepayAmounts.get("amount")); + assertEquals(0.0f, prepayAmounts.get("interestPortion")); + assertEquals(100.0f, prepayAmounts.get("principalPortion")); + // On 1st day + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 1, 1)); + assertEquals(100.0f, prepayAmounts.get("amount")); + assertEquals(0.0f, prepayAmounts.get("interestPortion")); + assertEquals(100.0f, prepayAmounts.get("principalPortion")); + // On 2nd day + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 1, 2)); + assertEquals(100.02f, prepayAmounts.get("amount")); + assertEquals(0.02f, prepayAmounts.get("interestPortion")); + assertEquals(100.0f, prepayAmounts.get("principalPortion")); + // On due date of 1st period + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 2, 1)); + assertEquals(100.58f, prepayAmounts.get("amount")); + assertEquals(0.58f, prepayAmounts.get("interestPortion")); + assertEquals(100.0f, prepayAmounts.get("principalPortion")); + // On the 1st day of 2nd period + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 2, 2)); + assertEquals(100.60f, prepayAmounts.get("amount")); + assertEquals(0.60f, prepayAmounts.get("interestPortion")); + assertEquals(100.0f, prepayAmounts.get("principalPortion")); + // In the middle of 2nd period (15 Feb) + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 2, 15)); + assertEquals(100.86f, prepayAmounts.get("amount")); + assertEquals(0.86f, prepayAmounts.get("interestPortion")); + assertEquals(100.0f, prepayAmounts.get("principalPortion")); + // On the due date of 2nd period + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 3, 1)); + assertEquals(101.16f, prepayAmounts.get("amount")); + assertEquals(1.16f, prepayAmounts.get("interestPortion")); + assertEquals(100.0f, prepayAmounts.get("principalPortion")); + }); + String repaymentDate = "01 February 2024"; + runAt(repaymentDate, () -> { + loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest() + .transactionDate(repaymentDate).dateFormat("dd MMMM yyyy").locale("en").transactionAmount(17.01)); + // Before 1st disbursement date + HashMap prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2023, 12, 31)); + assertEquals(83.57f, prepayAmounts.get("amount")); + assertEquals(0.0f, prepayAmounts.get("interestPortion")); + assertEquals(83.57f, prepayAmounts.get("principalPortion")); + // On 1st day + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 1, 1)); + assertEquals(83.57f, prepayAmounts.get("amount")); + assertEquals(0.0f, prepayAmounts.get("interestPortion")); + assertEquals(83.57f, prepayAmounts.get("principalPortion")); + // On 2nd day + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 1, 2)); + assertEquals(83.57f, prepayAmounts.get("amount")); + assertEquals(0.00f, prepayAmounts.get("interestPortion")); + assertEquals(83.57f, prepayAmounts.get("principalPortion")); + // On due date of 1st period + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 2, 1)); + assertEquals(83.57f, prepayAmounts.get("amount")); + assertEquals(0.0f, prepayAmounts.get("interestPortion")); + assertEquals(83.57f, prepayAmounts.get("principalPortion")); + // On the 1st day of 2nd period + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 2, 2)); + assertEquals(83.59f, prepayAmounts.get("amount")); + assertEquals(0.02f, prepayAmounts.get("interestPortion")); + assertEquals(83.57f, prepayAmounts.get("principalPortion")); + // In the middle of 2nd period (15 Feb) + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 2, 15)); + assertEquals(83.81f, prepayAmounts.get("amount")); + assertEquals(0.24f, prepayAmounts.get("interestPortion")); + assertEquals(83.57f, prepayAmounts.get("principalPortion")); + // On the due date of 2nd period + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 3, 1)); + assertEquals(84.06f, prepayAmounts.get("amount")); + assertEquals(0.49f, prepayAmounts.get("interestPortion")); + assertEquals(83.57f, prepayAmounts.get("principalPortion")); + }); + + String secondRepaymentDate = "15 February 2024"; + runAt(secondRepaymentDate, () -> { + loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest() + .transactionDate(secondRepaymentDate).dateFormat("dd MMMM yyyy").locale("en").transactionAmount(5.0)); + // Before 1st disbursement date + HashMap prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2023, 12, 31)); + assertEquals(78.57f, prepayAmounts.get("amount")); + assertEquals(0.0f, prepayAmounts.get("interestPortion")); + assertEquals(78.57f, prepayAmounts.get("principalPortion")); + // On 1st day + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 1, 1)); + assertEquals(78.57f, prepayAmounts.get("amount")); + assertEquals(0.0f, prepayAmounts.get("interestPortion")); + assertEquals(78.57f, prepayAmounts.get("principalPortion")); + // On 2nd day + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 1, 2)); + assertEquals(78.57f, prepayAmounts.get("amount")); + assertEquals(0.00f, prepayAmounts.get("interestPortion")); + assertEquals(78.57f, prepayAmounts.get("principalPortion")); + // On due date of 1st period + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 2, 1)); + assertEquals(78.57f, prepayAmounts.get("amount")); + assertEquals(0.0f, prepayAmounts.get("interestPortion")); + assertEquals(78.57f, prepayAmounts.get("principalPortion")); + // On the 1st day of 2nd period + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 2, 2)); + assertEquals(78.59f, prepayAmounts.get("amount")); + assertEquals(0.02f, prepayAmounts.get("interestPortion")); + assertEquals(78.57f, prepayAmounts.get("principalPortion")); + // In the middle of 2nd period (15 Feb) + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 2, 15)); + assertEquals(78.81f, prepayAmounts.get("amount")); + assertEquals(0.24f, prepayAmounts.get("interestPortion")); + assertEquals(78.57f, prepayAmounts.get("principalPortion")); + // On the due date of 2nd period + prepayAmounts = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, createdLoanId.intValue(), + LocalDate.of(2024, 3, 1)); + assertEquals(79.05f, prepayAmounts.get("amount")); + assertEquals(0.48f, prepayAmounts.get("interestPortion")); + assertEquals(78.57f, prepayAmounts.get("principalPortion")); + + loanTransactionHelper.makeLoanRepayment(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest() + .transactionDate(secondRepaymentDate).dateFormat("dd MMMM yyyy").locale("en").transactionAmount(78.81)); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + }); + + } + private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId, Integer numberOfRepayments, String loanDisbursementDate, double amount) { LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 3a7cd4ad9fe..54159aefd54 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -867,8 +867,8 @@ protected PostLoansRequest applyLoanRequest(Long clientId, Long loanProductId, S return postLoansRequest; } - protected PostLoansRequest applyPin4ProgressiveLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, - Double amount, Double interestRate, int numberOfRepayments, Consumer customizer) { + protected PostLoansRequest applyLP2ProgressiveLoanRequest(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount, + Double interestRate, int numberOfRepayments, Consumer customizer) { PostLoansRequest postLoansRequest = new PostLoansRequest().clientId(clientId) .transactionProcessingStrategyCode(ADVANCED_PAYMENT_ALLOCATION_STRATEGY).productId(loanProductId) @@ -912,7 +912,7 @@ protected Long applyAndApproveLoan(Long clientId, Long loanProductId, String loa protected Long applyAndApproveProgressiveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount, Double interestRate, int numberOfRepayments, Consumer customizer) { - PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyPin4ProgressiveLoanRequest(clientId, loanProductId, + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, loanProductId, loanDisbursementDate, amount, interestRate, numberOfRepayments, customizer)); PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPayOffTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPayOffTest.java index 6cb034b6cac..164e4120a69 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPayOffTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPayOffTest.java @@ -44,7 +44,7 @@ public void beforeEach() { clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive()); PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan( - applyPin4ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null)); + applyLP2ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null)); loanId = postLoansResponse.getLoanId(); loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 June 2024")); disburseLoan(loanId, BigDecimal.valueOf(250.0), "01 June 2024"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java index 95819058daa..cc9b93e6ac6 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java @@ -318,7 +318,7 @@ private Long createLoanForRefundWithInterestRefund(Long clientId, String refundT final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct( create4IProgressive().supportedInterestRefundTypes(new ArrayList<>()).addSupportedInterestRefundTypesItem(refundType)); PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan( - applyPin4ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null)); + applyLP2ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null)); Long loanId = postLoansResponse.getLoanId(); loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 June 2024")); disburseLoan(loanId, BigDecimal.valueOf(1000.0), "01 June 2024"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionBackdatedProgressiveTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionBackdatedProgressiveTest.java index 52598308719..5d2176527c8 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionBackdatedProgressiveTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionBackdatedProgressiveTest.java @@ -44,7 +44,7 @@ public void beforeEach() { clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive()); PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan( - applyPin4ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null)); + applyLP2ProgressiveLoanRequest(clientId, loanProductsResponse.getResourceId(), "01 June 2024", 1000.0, 10.0, 4, null)); loanId = postLoansResponse.getLoanId(); loanTransactionHelper.approveLoan(loanId, approveLoanRequest(1000.0, "01 June 2024")); disburseLoan(loanId, BigDecimal.valueOf(250.0), "01 June 2024"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 128573f6666..a8246150ac1 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -1488,6 +1488,15 @@ public HashMap getPrepayAmount(final RequestSpecification requestSpec, final Res return response; } + public HashMap getPrepayAmount(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, final Integer loanID, + final LocalDate transactionDate) { + final String URL = "/fineract-provider/api/v1/loans/" + loanID + + "/transactions/template?command=prepayLoan&locale=en&dateFormat=yyyy-MM-dd&transactionDate=" + transactionDate + "&" + + Utils.TENANT_IDENTIFIER; + final HashMap response = Utils.performServerGet(requestSpec, responseSpec, URL, ""); + return response; + } + private String createLoanRefundTransferURL() { return "/fineract-provider/api/v1/accounttransfers/refundByTransfer?tenantIdentifier=default"; }