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 926310f76..b495af02f 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 @@ -182,6 +182,16 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { .principal(outstandingAmounts.getOutstandingPrincipal()) // .interest(outstandingAmounts.getOutstandingInterest());// + // We need to deduct any paid amount if there is no interest recalculation + if (!loan.isInterestRecalculationEnabled()) { + BigDecimal paidInterest = installments.stream().map(LoanRepaymentScheduleInstallment::getInterestPaid).reduce(BigDecimal.ZERO, + BigDecimal::add); + BigDecimal paidPrincipal = installments.stream().map(LoanRepaymentScheduleInstallment::getPrincipal).reduce(BigDecimal.ZERO, + BigDecimal::add); + result.principal().minus(paidPrincipal); + result.interest().minus(paidInterest); + } + installments.forEach(installment -> { if (installment.isAdditional()) { result.plusPrincipal(installment.getPrincipalOutstanding(currency)) diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java index 7dfa1e075..e29bfc51c 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/data/ProgressiveLoanInterestScheduleModel.java @@ -298,7 +298,7 @@ public class ProgressiveLoanInterestScheduleModel { * @return */ public Money getTotalDueInterest() { - return repaymentPeriods().stream().map(RepaymentPeriod::getCalculatedDueInterest).reduce(zero(), Money::plus); + return repaymentPeriods().stream().map(RepaymentPeriod::getDueInterest).reduce(zero(), Money::plus); } /** 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 a20a0054a..33c1b341e 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 @@ -1302,10 +1302,9 @@ class ProgressiveEMICalculatorTest { final LocalDate dueDate = LocalDate.of(2024, 2, 1); final LocalDate startDay = LocalDate.of(2024, 1, 1); - // TODO: work on interest calculation - // emiCalculator.payInterest(interestModel, dueDate, startDay.plusDays(3), toMoney(0.56)); - // emiCalculator.chargebackInterest(interestModel, startDay.plusDays(3), toMoney(0.0)); - // emiCalculator.addBalanceCorrection(interestModel, startDay.plusDays(3), toMoney(0.0)); + emiCalculator.payInterest(interestModel, dueDate, startDay.plusDays(3), toMoney(0.56)); + emiCalculator.chargebackInterest(interestModel, startDay.plusDays(3), toMoney(0.0)); + emiCalculator.addBalanceCorrection(interestModel, startDay.plusDays(3), toMoney(0.0)); checkDailyInterest(interestModel, dueDate, startDay, 1, 0.19, 0.19); checkDailyInterest(interestModel, dueDate, startDay, 2, 0.19, 0.38); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java index bda2bf37b..3949077b8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CommonLoanSummaryDataProvider.java @@ -87,13 +87,13 @@ public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataPr totalRepaymentTransactionReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, LoanTransactionType.REPAYMENT); - if (repaymentSchedule != null) { + if (repaymentSchedule != null && defaultSummaryData.getInterestCharged().compareTo(BigDecimal.ZERO) > 0) { // Outstanding Interest on Past due installments totalUnpaidPayableDueInterest = computeTotalUnpaidPayableDueInterestAmount(repaymentSchedule.getPeriods(), businessDate); // Accumulated daily interest of the current Installment period totalUnpaidPayableNotDueInterest = computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(loan, - repaymentSchedule.getPeriods(), businessDate, defaultSummaryData.getCurrency()); + repaymentSchedule.getPeriods(), businessDate, defaultSummaryData.getCurrency(), totalUnpaidPayableDueInterest); } return LoanSummaryData.builder().currency(defaultSummaryData.getCurrency()) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java index b798baa39..b1d3d33fe 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/CumulativeLoanSummaryDataProvider.java @@ -44,7 +44,8 @@ public class CumulativeLoanSummaryDataProvider extends CommonLoanSummaryDataProv @Override public BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(final Loan loan, - final Collection periods, final LocalDate businessDate, final CurrencyData currency) { + final Collection periods, final LocalDate businessDate, final CurrencyData currency, + BigDecimal totalUnpaidPayableDueInterest) { // Find the current Period (If exists one) based on the Business date final Optional optCurrentPeriod = periods.stream().filter(period -> !period.isDownPaymentPeriod() // && period.getPeriod() != null // diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java index b10f86f74..aef7f8d4c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanSummaryDataProvider.java @@ -33,7 +33,7 @@ public interface LoanSummaryDataProvider { BigDecimal computeTotalUnpaidPayableDueInterestAmount(Collection periods, LocalDate businessDate); BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(Loan loan, Collection periods, - LocalDate businessDate, CurrencyData currency); + LocalDate businessDate, CurrencyData currency, BigDecimal totalUnpaidPayableDueInterest); LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java index e1c9e0c45..bbae06809 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/ProgressiveLoanSummaryDataProvider.java @@ -21,9 +21,9 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.math.BigDecimal; import java.time.LocalDate; import java.util.Collection; -import java.util.Comparator; import java.util.List; import java.util.Objects; +import java.util.Optional; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; @@ -40,7 +40,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.imp import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; -import org.apache.fineract.portfolio.loanproduct.calc.data.PeriodDueDetails; +import org.apache.fineract.portfolio.loanproduct.calc.data.OutstandingDetails; import org.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel; import org.springframework.stereotype.Component; @@ -71,26 +71,25 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro return super.withTransactionAmountsSummary(loan, defaultSummaryData, repaymentSchedule, loanTransactionBalances); } - private LoanRepaymentScheduleInstallment getRelatedRepaymentScheduleInstallment(Loan loan, LocalDate businessDate) { + private Optional getRelatedRepaymentScheduleInstallment(Loan loan, LocalDate businessDate) { return loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isDownPayment() && !i.isAdditional() - && !businessDate.isBefore(i.getFromDate()) && businessDate.isBefore(i.getDueDate())).findFirst().orElseGet(() -> { - List list = loan.getRepaymentScheduleInstallments().stream() - .filter(i -> !i.isDownPayment() && !i.isAdditional()).toList(); - return !list.isEmpty() ? list.get(list.size() - 1) : null; - }); + && businessDate.isAfter(i.getFromDate()) && !businessDate.isAfter(i.getDueDate())).findFirst(); } @Override public BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(final Loan loan, - final Collection periods, final LocalDate businessDate, final CurrencyData currency) { - if (loan.isMatured(businessDate)) { + final Collection periods, final LocalDate businessDate, final CurrencyData currency, + BigDecimal totalUnpaidPayableDueInterest) { + if (loan.isMatured(businessDate) || !loan.isInterestBearing()) { return BigDecimal.ZERO; } - LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = getRelatedRepaymentScheduleInstallment(loan, businessDate); - if (loan.isInterestBearing() && loanRepaymentScheduleInstallment != null) { + Optional currentRepaymentPeriod = getRelatedRepaymentScheduleInstallment(loan, businessDate); + + if (currentRepaymentPeriod.isPresent()) { if (loan.isChargedOff()) { - return loanRepaymentScheduleInstallment.getInterestOutstanding(loan.getCurrency()).getAmount(); + return MathUtil.subtractToZero(currentRepaymentPeriod.get().getInterestOutstanding(loan.getCurrency()).getAmount(), + totalUnpaidPayableDueInterest); } else { List transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream() .filter(t -> !t.isAccrualActivity()).toList(); @@ -107,20 +106,15 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro replayedTransactions); } if (model != null) { - LoanRepaymentScheduleInstallment nextUnpaidInAdvanceInstallment = loanRepaymentScheduleInstallment.isNotFullyPaidOff() - ? loanRepaymentScheduleInstallment - : loan.getRepaymentScheduleInstallments().stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) - .filter(i -> i.getInstallmentNumber() != null) - .min(Comparator.comparingInt(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); - if (nextUnpaidInAdvanceInstallment == null) { - return BigDecimal.ZERO; - } - PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(model, nextUnpaidInAdvanceInstallment.getDueDate(), - businessDate); - if (dueAmounts != null) { - BigDecimal interestPaid = nextUnpaidInAdvanceInstallment.getInterestPaid(); - BigDecimal dueInterest = dueAmounts.getDueInterest().getAmount(); - return MathUtil.subtractToZero(dueInterest, interestPaid); + OutstandingDetails outstandingDetails = emiCalculator.getOutstandingAmountsTillDate(model, businessDate); + if (!loan.isInterestRecalculationEnabled()) { + BigDecimal interestPaid = periods.stream().map(LoanSchedulePeriodData::getInterestPaid).reduce(BigDecimal.ZERO, + BigDecimal::add); + BigDecimal dueInterest = outstandingDetails.getOutstandingInterest().getAmount(); + return MathUtil.subtractToZero(dueInterest, interestPaid, totalUnpaidPayableDueInterest); + } else { + return MathUtil.subtractToZero(outstandingDetails.getOutstandingInterest().getAmount(), + totalUnpaidPayableDueInterest); } } } diff --git a/gradle.properties b/gradle.properties index 022d5cbce..84dd6ec6a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,7 +16,7 @@ # specific language governing permissions and limitations # under the License. # -org.gradle.jvmargs=-Xmx6g --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=null +org.gradle.jvmargs=-Xmx12g --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=null buildType=BUILD org.gradle.caching=true org.gradle.parallel=true diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPrepayAmountTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPrepayAmountTest.java index d5bfee134..96d395c66 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPrepayAmountTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanPrepayAmountTest.java @@ -19,9 +19,12 @@ package org.apache.fineract.integrationtests; import java.math.BigDecimal; -import java.util.HashMap; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.integrationtests.common.ClientHelper; @@ -51,11 +54,73 @@ public class LoanPrepayAmountTest extends BaseLoanIntegrationTest { for (int i = 7; i <= 31; i++) { runAt(i + " January 2024", () -> { GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); - HashMap prepayAmount = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, loanId.intValue()); - Assertions.assertEquals((float) prepayAmount.get("interestPortion"), - loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().floatValue()); + GetLoansLoanIdTransactionsTemplateResponse prepayAmountResponse = loanTransactionHelper.getPrepaymentAmount(loanId, null, + DATETIME_PATTERN); + Assertions.assertEquals(BigDecimal.valueOf(prepayAmountResponse.getInterestPortion()).stripTrailingZeros(), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); }); } } + @Test + public void testLoanPrepayAmountProgressivePartialRepayment() { + runAt("15 March 2025", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct( + create4IProgressive().interestRatePerPeriod(35.99).numberOfRepayments(12).isInterestRecalculationEnabled(true)); + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, + loanProductsResponse.getResourceId(), "15 March 2025", 296.79, 35.99, 12, null)); + loanId = postLoansResponse.getLoanId(); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(296.79, "15 March 2025")); + disburseLoan(loanId, BigDecimal.valueOf(296.79), "15 March 2025"); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("16 March 2025", () -> { + loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "16 March 2025", 59.0); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + GetLoansLoanIdTransactionsTemplateResponse prepayAmountResponse = loanTransactionHelper.getPrepaymentAmount(loanId, + "16 March 2025", DATETIME_PATTERN); + Assertions.assertEquals(BigDecimal.valueOf(prepayAmountResponse.getInterestPortion()).stripTrailingZeros(), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + for (int i = 0; i <= 45; i++) { + LocalDate date = LocalDate.of(2025, 3, 17).plusDays(i); + String formattedDate = DateTimeFormatter.ofPattern(DATETIME_PATTERN).format(date); + runAt(formattedDate, () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + GetLoansLoanIdTransactionsTemplateResponse prepayAmountResponse = loanTransactionHelper.getPrepaymentAmount(loanId, + formattedDate, DATETIME_PATTERN); + Assertions.assertEquals(loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros(), + BigDecimal.valueOf(prepayAmountResponse.getInterestPortion()).stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + } + } + + @Test + public void testLoanPrepayAmountProgressivePartialRepaymentNoInterestRecalculation() { + runAt("15 March 2025", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct( + create4IProgressive().interestRatePerPeriod(35.99).numberOfRepayments(12).isInterestRecalculationEnabled(false)); + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, + loanProductsResponse.getResourceId(), "15 March 2025", 296.79, 35.99, 12, null)); + loanId = postLoansResponse.getLoanId(); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(296.79, "15 March 2025")); + disburseLoan(loanId, BigDecimal.valueOf(296.79), "15 March 2025"); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("16 March 2025", () -> { + loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "16 March 2025", 59.0); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + GetLoansLoanIdTransactionsTemplateResponse prepayAmountResponse = loanTransactionHelper.getPrepaymentAmount(loanId, + "16 March 2025", DATETIME_PATTERN); + Assertions.assertEquals(BigDecimal.valueOf(44.43).stripTrailingZeros(), + BigDecimal.valueOf(prepayAmountResponse.getInterestPortion()).stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java index 17d0e2ca9..71b21378c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanSummaryTest.java @@ -49,15 +49,17 @@ public class LoanSummaryTest extends BaseLoanIntegrationTest { runAt("15 January 2024", () -> { inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertEquals(BigDecimal.valueOf(3.05), loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest()); + Assertions.assertEquals(BigDecimal.valueOf(3.05), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "15 January 2024", 171.43); loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertEquals(0, loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().compareTo(BigDecimal.ZERO)); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); }); runAt("16 January 2024", () -> { inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertEquals(BigDecimal.valueOf(0.22), loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest()); + Assertions.assertEquals(BigDecimal.valueOf(0.22), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); verifyTransactions(loanId, transaction(250.0, "Disbursement", "01 January 2024"), transaction(350.0, "Disbursement", "04 January 2024"), transaction(400.0, "Disbursement", "05 January 2024"), transaction(2.78, "Accrual", "14 January 2024"), transaction(171.43, "Repayment", "15 January 2024"), @@ -66,7 +68,8 @@ public class LoanSummaryTest extends BaseLoanIntegrationTest { runAt("17 January 2024", () -> { inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertEquals(BigDecimal.valueOf(0.44), loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest()); + Assertions.assertEquals(BigDecimal.valueOf(0.44), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); verifyTransactions(loanId, transaction(250.0, "Disbursement", "01 January 2024"), transaction(350.0, "Disbursement", "04 January 2024"), transaction(400.0, "Disbursement", "05 January 2024"), transaction(2.78, "Accrual", "14 January 2024"), transaction(171.43, "Repayment", "15 January 2024"), @@ -75,7 +78,8 @@ public class LoanSummaryTest extends BaseLoanIntegrationTest { runAt("18 January 2024", () -> { inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); - Assertions.assertEquals(BigDecimal.valueOf(0.67), loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest()); + Assertions.assertEquals(BigDecimal.valueOf(0.67), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); verifyTransactions(loanId, transaction(250.0, "Disbursement", "01 January 2024"), transaction(350.0, "Disbursement", "04 January 2024"), transaction(400.0, "Disbursement", "05 January 2024"), transaction(2.78, "Accrual", "14 January 2024"), transaction(171.43, "Repayment", "15 January 2024"), @@ -91,4 +95,140 @@ public class LoanSummaryTest extends BaseLoanIntegrationTest { transaction(0.22, "Accrual", "17 January 2024"), transaction(0.23, "Accrual", "18 January 2024")); }); } + + @Test + public void testUnpaidPayableNotDueInterestForProgressiveLoanInCaseOfEarlyRepaymentAlmostFullyPaid2ndPeriod() { + runAt("15 March 2025", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct( + create4IProgressive().interestRatePerPeriod(35.99).numberOfRepayments(12).isInterestRecalculationEnabled(true)); + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(clientId, + loanProductsResponse.getResourceId(), "15 March 2025", 296.79, 35.99, 12, null)); + loanId = postLoansResponse.getLoanId(); + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(296.79, "15 March 2025")); + disburseLoan(loanId, BigDecimal.valueOf(296.79), "15 March 2025"); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("16 March 2025", () -> { + loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "16 March 2025", 59.0); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("17 March 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(0.23), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("18 March 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(0.46), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("19 March 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(0.69), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("20 March 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(0.92), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("21 March 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(1.15), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("22 March 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(1.38), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("14 May 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(13.81), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + + runAt("15 May 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(14.05), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("16 May 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(14.28), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("17 May 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(14.51), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("18 May 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(14.74), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("19 May 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(14.97), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("20 May 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(15.20), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("14 June 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(20.96), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("15 June 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.valueOf(21.19), + loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.ZERO, loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + runAt("16 June 2025", () -> { + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + Assertions.assertEquals(BigDecimal.valueOf(21.19), + loanDetails.getSummary().getTotalUnpaidPayableDueInterest().stripTrailingZeros()); + Assertions.assertEquals(BigDecimal.valueOf(0.24), + loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().stripTrailingZeros()); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); + }); + } + } 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 91b9ff3b1..125d70fe6 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 @@ -2301,6 +2301,12 @@ public class LoanTransactionHelper { return response; } + public GetLoansLoanIdTransactionsTemplateResponse getPrepaymentAmount(final Long loanId, final String transactionDate, + String dateformat) { + return Calls.ok(FineractClientHelper.getFineractClient().loanTransactions.retrieveTransactionTemplate(loanId, "prepayLoan", + dateformat, transactionDate, "en")); + } + // TODO: Rewrite to use fineract-client instead! // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // org.apache.fineract.client.models.PostLoansLoanIdRequest)