diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java index fb53d65ba..b33a17f11 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -427,9 +427,9 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing AccrualPeriodData period = accrualPeriods.getPeriodByInstallmentNumber(installment.getInstallmentNumber()); MonetaryCurrency currency = accrualPeriods.getCurrency(); Money interest = null; - boolean isFullPeriod = isFullPeriod(tillDate, installment); + boolean isPastPeriod = isAfterPeriod(tillDate, installment); boolean isInPeriod = isInPeriod(tillDate, installment, false); - if (isFullPeriod) { + if (isPastPeriod || loan.isClosed() || loan.isOverPaid()) { interest = installment.getInterestCharged(currency); } else { if (isInPeriod) { // first period first day is not accrued @@ -447,7 +447,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing unrecognizedWaived = MathUtil.min(unrecognizedWaived, MathUtil.minusToZero(installment.getInterestWaived(currency), transactionWaived), false); period.setUnrecognizedWaive(unrecognizedWaived); - Money waived = isFullPeriod ? installment.getInterestWaived(currency) : MathUtil.plus(transactionWaived, unrecognizedWaived); + Money waived = isPastPeriod ? installment.getInterestWaived(currency) : MathUtil.plus(transactionWaived, unrecognizedWaived); accruable = MathUtil.minusToZero(period.getInterestAmount(), waived); } period.setInterestAccruable(accruable); @@ -499,7 +499,7 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing .map(p -> MathUtil.toBigDecimal(p.getTransactionAccrued())).reduce(BigDecimal.ZERO, MathUtil::add); BigDecimal accrued = MathUtil.subtractToZero(totalAccrued, prevAccrued); // if this is the current-last period, all the remaining accrued amount is added - return isInPeriod(tillDate, installment, false) ? accrued : MathUtil.min(installment.getInterestCharged(), accrued, false); + return isInPeriod(tillDate, installment, false) ? accrued : MathUtil.min(installment.getInterestAccrued(), accrued, false); } else { return isFullPeriod(tillDate, installment) ? installment.getInterestAccrued() : loan.getLoanTransactions().stream() diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java index 59476b965..713eba7e8 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCOBCreateAccrualsTest.java @@ -26,8 +26,10 @@ import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.integrationtests.common.ClientHelper; import org.junit.Test; +import org.junit.jupiter.api.Assertions; @Slf4j public class LoanCOBCreateAccrualsTest extends BaseLoanIntegrationTest { @@ -423,4 +425,61 @@ public class LoanCOBCreateAccrualsTest extends BaseLoanIntegrationTest { }); } + + @Test + public void testEarlyRepaymentAccruals() { + AtomicReference loanIdRef = new AtomicReference<>(); + setup(); + final PostLoanProductsResponse loanProductsResponse = loanProductHelper.createLoanProduct(create4IProgressive()); + + runAt("20 December 2024", () -> { + Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProductsResponse.getResourceId(), "20 December 2024", + 430.0, 26.0, 6, null); + + loanIdRef.set(loanId); + + disburseLoan(loanId, BigDecimal.valueOf(430), "20 December 2024"); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + validateFullyUnpaidRepaymentPeriod(loanDetails, 1, "20 January 2025", 67.88, 0, 0, 9.32); + validateFullyUnpaidRepaymentPeriod(loanDetails, 2, "20 February 2025", 69.35, 0, 0, 7.85); + validateFullyUnpaidRepaymentPeriod(loanDetails, 3, "20 March 2025", 70.86, 0, 0, 6.34); + validateFullyUnpaidRepaymentPeriod(loanDetails, 4, "20 April 2025", 72.39, 0, 0, 4.81); + validateFullyUnpaidRepaymentPeriod(loanDetails, 5, "20 May 2025", 73.96, 0, 0, 3.24); + validateFullyUnpaidRepaymentPeriod(loanDetails, 6, "20 June 2025", 75.56, 0, 0, 1.64); + + verifyTransactions(loanId, transaction(430.0d, "Disbursement", "20 December 2024")); + executeInlineCOB(loanId); + }); + runAt("30 December 2024", () -> { + Long loanId = loanIdRef.get(); + loanTransactionHelper.makeLoanRepayment(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN) + .transactionDate("30 December 2024").locale("en").transactionAmount(200.0)); + }); + runAt("22 March 2025", () -> { + Long loanId = loanIdRef.get(); + executeInlineCOB(loanId); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId); + + // No unexpected big accruals or any accrual adjustments + Assertions.assertTrue(loanDetails.getTransactions().stream().noneMatch(t -> (t.getType().getAccrual() && t.getAmount() > 0.31) + || "loanTransactionType.accrualAdjustment".equals(t.getType().getCode()))); + + // Accruals around installment due dates are as expected + Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( + t -> t.getDate().equals(LocalDate.of(2025, 1, 20)) && t.getType().getAccrual() && t.getAmount().equals(0.16D))); + Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( + t -> t.getDate().equals(LocalDate.of(2025, 1, 21)) && t.getType().getAccrual() && t.getAmount().equals(0.16D))); + Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( + t -> t.getDate().equals(LocalDate.of(2025, 2, 20)) && t.getType().getAccrual() && t.getAmount().equals(0.16D))); + Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( + t -> t.getDate().equals(LocalDate.of(2025, 2, 21)) && t.getType().getAccrual() && t.getAmount().equals(0.18D))); + Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( + t -> t.getDate().equals(LocalDate.of(2025, 3, 20)) && t.getType().getAccrual() && t.getAmount().equals(0.18D))); + Assertions.assertTrue(loanDetails.getTransactions().stream().anyMatch( + t -> t.getDate().equals(LocalDate.of(2025, 3, 21)) && t.getType().getAccrual() && t.getAmount().equals(0.16D))); + }); + + } }