FINERACT-2221: Fix - Interest not counted towards totalUnpaidPayableNotDueInterest after partial repayment

This commit is contained in:
Adam Saghy
2025-03-24 14:23:00 +01:00
parent 2251ee0da6
commit b2af4a635c
11 changed files with 261 additions and 46 deletions
@@ -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));
});
}
}
@@ -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));
});
}
}
@@ -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)