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)