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
@@ -182,6 +182,16 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
.principal(outstandingAmounts.getOutstandingPrincipal()) // .principal(outstandingAmounts.getOutstandingPrincipal()) //
.interest(outstandingAmounts.getOutstandingInterest());// .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 -> { installments.forEach(installment -> {
if (installment.isAdditional()) { if (installment.isAdditional()) {
result.plusPrincipal(installment.getPrincipalOutstanding(currency)) result.plusPrincipal(installment.getPrincipalOutstanding(currency))
@@ -298,7 +298,7 @@ public class ProgressiveLoanInterestScheduleModel {
* @return * @return
*/ */
public Money getTotalDueInterest() { public Money getTotalDueInterest() {
return repaymentPeriods().stream().map(RepaymentPeriod::getCalculatedDueInterest).reduce(zero(), Money::plus); return repaymentPeriods().stream().map(RepaymentPeriod::getDueInterest).reduce(zero(), Money::plus);
} }
/** /**
@@ -1302,10 +1302,9 @@ class ProgressiveEMICalculatorTest {
final LocalDate dueDate = LocalDate.of(2024, 2, 1); final LocalDate dueDate = LocalDate.of(2024, 2, 1);
final LocalDate startDay = LocalDate.of(2024, 1, 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.payInterest(interestModel, dueDate, startDay.plusDays(3), toMoney(0.56)); emiCalculator.chargebackInterest(interestModel, startDay.plusDays(3), toMoney(0.0));
// emiCalculator.chargebackInterest(interestModel, startDay.plusDays(3), toMoney(0.0)); emiCalculator.addBalanceCorrection(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, 1, 0.19, 0.19);
checkDailyInterest(interestModel, dueDate, startDay, 2, 0.19, 0.38); checkDailyInterest(interestModel, dueDate, startDay, 2, 0.19, 0.38);
@@ -87,13 +87,13 @@ public abstract class CommonLoanSummaryDataProvider implements LoanSummaryDataPr
totalRepaymentTransactionReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, totalRepaymentTransactionReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances,
LoanTransactionType.REPAYMENT); LoanTransactionType.REPAYMENT);
if (repaymentSchedule != null) { if (repaymentSchedule != null && defaultSummaryData.getInterestCharged().compareTo(BigDecimal.ZERO) > 0) {
// Outstanding Interest on Past due installments // Outstanding Interest on Past due installments
totalUnpaidPayableDueInterest = computeTotalUnpaidPayableDueInterestAmount(repaymentSchedule.getPeriods(), businessDate); totalUnpaidPayableDueInterest = computeTotalUnpaidPayableDueInterestAmount(repaymentSchedule.getPeriods(), businessDate);
// Accumulated daily interest of the current Installment period // Accumulated daily interest of the current Installment period
totalUnpaidPayableNotDueInterest = computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(loan, totalUnpaidPayableNotDueInterest = computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(loan,
repaymentSchedule.getPeriods(), businessDate, defaultSummaryData.getCurrency()); repaymentSchedule.getPeriods(), businessDate, defaultSummaryData.getCurrency(), totalUnpaidPayableDueInterest);
} }
return LoanSummaryData.builder().currency(defaultSummaryData.getCurrency()) return LoanSummaryData.builder().currency(defaultSummaryData.getCurrency())
@@ -44,7 +44,8 @@ public class CumulativeLoanSummaryDataProvider extends CommonLoanSummaryDataProv
@Override @Override
public BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(final Loan loan, 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 // Find the current Period (If exists one) based on the Business date
final Optional<LoanSchedulePeriodData> optCurrentPeriod = periods.stream().filter(period -> !period.isDownPaymentPeriod() // final Optional<LoanSchedulePeriodData> optCurrentPeriod = periods.stream().filter(period -> !period.isDownPaymentPeriod() //
&& period.getPeriod() != null // && period.getPeriod() != null //
@@ -33,7 +33,7 @@ public interface LoanSummaryDataProvider {
BigDecimal computeTotalUnpaidPayableDueInterestAmount(Collection<LoanSchedulePeriodData> periods, LocalDate businessDate); BigDecimal computeTotalUnpaidPayableDueInterestAmount(Collection<LoanSchedulePeriodData> periods, LocalDate businessDate);
BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(Loan loan, Collection<LoanSchedulePeriodData> periods, BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(Loan loan, Collection<LoanSchedulePeriodData> periods,
LocalDate businessDate, CurrencyData currency); LocalDate businessDate, CurrencyData currency, BigDecimal totalUnpaidPayableDueInterest);
LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData); LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData);
@@ -21,9 +21,9 @@ package org.apache.fineract.portfolio.loanaccount.service;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.Collection; import java.util.Collection;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.Optional;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.tuple.Pair; 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.LoanScheduleData;
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; 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.apache.fineract.portfolio.loanproduct.calc.data.ProgressiveLoanInterestScheduleModel;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -71,26 +71,25 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro
return super.withTransactionAmountsSummary(loan, defaultSummaryData, repaymentSchedule, loanTransactionBalances); 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() return loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isDownPayment() && !i.isAdditional()
&& !businessDate.isBefore(i.getFromDate()) && businessDate.isBefore(i.getDueDate())).findFirst().orElseGet(() -> { && businessDate.isAfter(i.getFromDate()) && !businessDate.isAfter(i.getDueDate())).findFirst();
List<LoanRepaymentScheduleInstallment> list = loan.getRepaymentScheduleInstallments().stream()
.filter(i -> !i.isDownPayment() && !i.isAdditional()).toList();
return !list.isEmpty() ? list.get(list.size() - 1) : null;
});
} }
@Override @Override
public BigDecimal computeTotalUnpaidPayableNotDueInterestAmountOnActualPeriod(final Loan loan, 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,
if (loan.isMatured(businessDate)) { BigDecimal totalUnpaidPayableDueInterest) {
if (loan.isMatured(businessDate) || !loan.isInterestBearing()) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = getRelatedRepaymentScheduleInstallment(loan, businessDate); Optional<LoanRepaymentScheduleInstallment> currentRepaymentPeriod = getRelatedRepaymentScheduleInstallment(loan, businessDate);
if (loan.isInterestBearing() && loanRepaymentScheduleInstallment != null) {
if (currentRepaymentPeriod.isPresent()) {
if (loan.isChargedOff()) { if (loan.isChargedOff()) {
return loanRepaymentScheduleInstallment.getInterestOutstanding(loan.getCurrency()).getAmount(); return MathUtil.subtractToZero(currentRepaymentPeriod.get().getInterestOutstanding(loan.getCurrency()).getAmount(),
totalUnpaidPayableDueInterest);
} else { } else {
List<LoanTransaction> transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream() List<LoanTransaction> transactionsToReprocess = loan.retrieveListOfTransactionsForReprocessing().stream()
.filter(t -> !t.isAccrualActivity()).toList(); .filter(t -> !t.isAccrualActivity()).toList();
@@ -107,20 +106,15 @@ public class ProgressiveLoanSummaryDataProvider extends CommonLoanSummaryDataPro
replayedTransactions); replayedTransactions);
} }
if (model != null) { if (model != null) {
LoanRepaymentScheduleInstallment nextUnpaidInAdvanceInstallment = loanRepaymentScheduleInstallment.isNotFullyPaidOff() OutstandingDetails outstandingDetails = emiCalculator.getOutstandingAmountsTillDate(model, businessDate);
? loanRepaymentScheduleInstallment if (!loan.isInterestRecalculationEnabled()) {
: loan.getRepaymentScheduleInstallments().stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) BigDecimal interestPaid = periods.stream().map(LoanSchedulePeriodData::getInterestPaid).reduce(BigDecimal.ZERO,
.filter(i -> i.getInstallmentNumber() != null) BigDecimal::add);
.min(Comparator.comparingInt(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); BigDecimal dueInterest = outstandingDetails.getOutstandingInterest().getAmount();
if (nextUnpaidInAdvanceInstallment == null) { return MathUtil.subtractToZero(dueInterest, interestPaid, totalUnpaidPayableDueInterest);
return BigDecimal.ZERO; } else {
} return MathUtil.subtractToZero(outstandingDetails.getOutstandingInterest().getAmount(),
PeriodDueDetails dueAmounts = emiCalculator.getDueAmounts(model, nextUnpaidInAdvanceInstallment.getDueDate(), totalUnpaidPayableDueInterest);
businessDate);
if (dueAmounts != null) {
BigDecimal interestPaid = nextUnpaidInAdvanceInstallment.getInterestPaid();
BigDecimal dueInterest = dueAmounts.getDueInterest().getAmount();
return MathUtil.subtractToZero(dueInterest, interestPaid);
} }
} }
} }
+1 -1
View File
@@ -16,7 +16,7 @@
# specific language governing permissions and limitations # specific language governing permissions and limitations
# under the License. # under the License.
# #
org.gradle.jvmargs=-Xmx6g --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=null org.gradle.jvmargs=-Xmx12g --add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED --add-opens=java.rmi/sun.rmi.transport=null
buildType=BUILD buildType=BUILD
org.gradle.caching=true org.gradle.caching=true
org.gradle.parallel=true org.gradle.parallel=true
@@ -19,9 +19,12 @@
package org.apache.fineract.integrationtests; package org.apache.fineract.integrationtests;
import java.math.BigDecimal; 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 lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.GetLoansLoanIdResponse; 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.PostLoanProductsResponse;
import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.ClientHelper;
@@ -51,11 +54,73 @@ public class LoanPrepayAmountTest extends BaseLoanIntegrationTest {
for (int i = 7; i <= 31; i++) { for (int i = 7; i <= 31; i++) {
runAt(i + " January 2024", () -> { runAt(i + " January 2024", () -> {
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId);
HashMap prepayAmount = loanTransactionHelper.getPrepayAmount(requestSpec, responseSpec, loanId.intValue()); GetLoansLoanIdTransactionsTemplateResponse prepayAmountResponse = loanTransactionHelper.getPrepaymentAmount(loanId, null,
Assertions.assertEquals((float) prepayAmount.get("interestPortion"), DATETIME_PATTERN);
loanDetails.getSummary().getTotalUnpaidPayableNotDueInterest().floatValue()); 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", () -> { runAt("15 January 2024", () -> {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(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); loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "15 January 2024", 171.43);
loanDetails = loanTransactionHelper.getLoanDetails(loanId); 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", () -> { runAt("16 January 2024", () -> {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(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"), verifyTransactions(loanId, transaction(250.0, "Disbursement", "01 January 2024"),
transaction(350.0, "Disbursement", "04 January 2024"), transaction(400.0, "Disbursement", "05 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"), 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", () -> { runAt("17 January 2024", () -> {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(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"), verifyTransactions(loanId, transaction(250.0, "Disbursement", "01 January 2024"),
transaction(350.0, "Disbursement", "04 January 2024"), transaction(400.0, "Disbursement", "05 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"), 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", () -> { runAt("18 January 2024", () -> {
inlineLoanCOBHelper.executeInlineCOB(List.of(loanId)); inlineLoanCOBHelper.executeInlineCOB(List.of(loanId));
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(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"), verifyTransactions(loanId, transaction(250.0, "Disbursement", "01 January 2024"),
transaction(350.0, "Disbursement", "04 January 2024"), transaction(400.0, "Disbursement", "05 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"), 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")); 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; 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! // TODO: Rewrite to use fineract-client instead!
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long, // Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
// org.apache.fineract.client.models.PostLoansLoanIdRequest) // org.apache.fineract.client.models.PostLoansLoanIdRequest)