This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new b2af4a635c FINERACT-2221: Fix - Interest not counted towards 
totalUnpaidPayableNotDueInterest after partial repayment
b2af4a635c is described below

commit b2af4a635ca9b749794ca6bee58b98cc0e1b9650
Author: Adam Saghy <[email protected]>
AuthorDate: Fri Mar 21 17:13:15 2025 +0100

    FINERACT-2221: Fix - Interest not counted towards 
totalUnpaidPayableNotDueInterest after partial repayment
---
 .../domain/ProgressiveLoanScheduleGenerator.java   |  10 ++
 .../data/ProgressiveLoanInterestScheduleModel.java |   2 +-
 .../calc/ProgressiveEMICalculatorTest.java         |   7 +-
 .../service/CommonLoanSummaryDataProvider.java     |   4 +-
 .../service/CumulativeLoanSummaryDataProvider.java |   3 +-
 .../service/LoanSummaryDataProvider.java           |   2 +-
 .../ProgressiveLoanSummaryDataProvider.java        |  48 +++----
 gradle.properties                                  |   2 +-
 .../integrationtests/LoanPrepayAmountTest.java     |  73 +++++++++-
 .../fineract/integrationtests/LoanSummaryTest.java | 150 ++++++++++++++++++++-
 .../common/loans/LoanTransactionHelper.java        |   6 +
 11 files changed, 261 insertions(+), 46 deletions(-)

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 926310f76f..b495af02f5 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 7dfa1e0758..e29bfc51c1 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 a20a0054a6..33c1b341eb 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 bda2bf37b3..3949077b88 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 b798baa393..b1d3d33fe8 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<LoanSchedulePeriodData> periods, final LocalDate 
businessDate, final CurrencyData currency) {
+            final Collection<LoanSchedulePeriodData> periods, final LocalDate 
businessDate, final CurrencyData currency,
+            BigDecimal totalUnpaidPayableDueInterest) {
         // Find the current Period (If exists one) based on the Business date
         final Optional<LoanSchedulePeriodData> 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 b10f86f742..aef7f8d4ce 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<LoanSchedulePeriodData> 
periods, LocalDate businessDate);
 
     BigDecimal 
computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(Loan loan, 
Collection<LoanSchedulePeriodData> 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 e1c9e0c45f..bbae068092 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<LoanRepaymentScheduleInstallment> 
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<LoanRepaymentScheduleInstallment> 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<LoanSchedulePeriodData> periods, final LocalDate 
businessDate, final CurrencyData currency) {
-        if (loan.isMatured(businessDate)) {
+            final Collection<LoanSchedulePeriodData> 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<LoanRepaymentScheduleInstallment> 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<LoanTransaction> 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 022d5cbcec..84dd6ec6ac 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-UN [...]
+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-U [...]
 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 d5bfee1343..96d395c66a 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 17d0e2ca9e..71b21378c6 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 91b9ff3b17..125d70fe61 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)


Reply via email to