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
commit ad8fe13064c47a91bfb094748fedf9c37a6bca39 Author: mark.vituska <[email protected]> AuthorDate: Wed May 28 14:17:22 2025 +0200 FINERACT-2232: fix capitalized income balance handling during reverse replay situations --- .../serialization/LoanRefundValidator.java | 4 +- .../service/LoanDownPaymentHandlerServiceImpl.java | 4 +- .../LoanCapitalizedIncomeTest.java | 74 ++++++++++++++++++++++ 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java index a8f9bad5bc..3a9c72e427 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java @@ -104,9 +104,7 @@ public final class LoanRefundValidator { if (loan.getLoanProduct().isMultiDisburseLoan() && adjustedTransaction == null) { final BigDecimal totalDisbursed = loan.getDisbursedAmount(); final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments(); - final BigDecimal totalCapitalizedIncome = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome).stream() - .filter(LoanTransaction::isNotReversed).map(LoanTransaction::getPrincipalPortion) - .reduce(BigDecimal.ZERO, BigDecimal::add); + final BigDecimal totalCapitalizedIncome = loan.getSummary().getTotalCapitalizedIncome(); final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted).add(totalCapitalizedIncome); if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0) { final String errorMessage = "The transaction amount cannot exceed threshold."; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java index 271756c407..fb4b8d80f2 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java @@ -174,9 +174,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler if (loan.getLoanProduct().isMultiDisburseLoan()) { final BigDecimal totalDisbursed = loan.getDisbursedAmount(); final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments(); - final BigDecimal totalCapitalizedIncome = loan.getLoanTransactions(LoanTransaction::isCapitalizedIncome).stream() - .filter(LoanTransaction::isNotReversed).map(LoanTransaction::getPrincipalPortion) - .reduce(BigDecimal.ZERO, BigDecimal::add); + final BigDecimal totalCapitalizedIncome = loan.getSummary().getTotalCapitalizedIncome(); final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted).add(totalCapitalizedIncome); if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0 && loan.getLoanProductRelatedDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java index b92341ec2f..eecb08bac6 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanCapitalizedIncomeTest.java @@ -21,6 +21,7 @@ package org.apache.fineract.integrationtests; import static org.junit.jupiter.api.Assertions.assertTrue; import java.math.BigDecimal; +import java.math.RoundingMode; import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import org.apache.fineract.client.models.GetLoansLoanIdResponse; @@ -30,6 +31,7 @@ import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; +import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.integrationtests.common.BusinessStepHelper; import org.apache.fineract.integrationtests.common.ClientHelper; @@ -683,4 +685,76 @@ public class LoanCapitalizedIncomeTest extends BaseLoanIntegrationTest { }); } + + @Test + public void testOverpaymentAmountWhenCapitalizedIncomeTransactionsAreReversed() { + final AtomicReference<Long> loanIdRef = new AtomicReference<>(); + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + runAt("01 March 2023", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressiveWithCapitalizedIncome()); + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + loanProductsResponse.getResourceId(), "01 March 2023", 10000.00, 12.00, 4, null)); + Long loanId = postLoansResponse.getLoanId(); + loanIdRef.set(loanId); + + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(10000.00, "01 March 2023")); + disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 March 2023"); + + loanTransactionHelper.addCapitalizedIncome(loanId, "01 March 2023", 500.00); + PostLoansLoanIdTransactionsResponse transactionsResponse = loanTransactionHelper.addCapitalizedIncome(loanId, "01 March 2023", + 500.00); + + loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "1 March 2023", 2000.00); + loanTransactionHelper.reverseLoanTransaction(loanId, transactionsResponse.getResourceId(), "1 March 2023"); + }); + + BigDecimal zero = BigDecimal.ZERO; + BigDecimal thousand = BigDecimal.valueOf(1000.0); + BigDecimal fiveHundred = BigDecimal.valueOf(500.0); + BigDecimal thousandFiveHundred = BigDecimal.valueOf(1500.0); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanIdRef.get()); + Assertions.assertEquals(thousand, loanDetails.getPrincipal().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(thousand, loanDetails.getSummary().getPrincipalDisbursed().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(fiveHundred, loanDetails.getSummary().getTotalCapitalizedIncome().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(thousandFiveHundred, loanDetails.getSummary().getTotalPrincipal().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(zero, loanDetails.getSummary().getPrincipalOutstanding().setScale(0, RoundingMode.HALF_UP)); + + Assertions.assertEquals(fiveHundred, loanDetails.getTotalOverpaid().setScale(1, RoundingMode.HALF_UP)); + } + + @Test + public void testOverpaymentAmountCorrectlyCalculatedWhenBackdatedRepaymentIsMade() { + final AtomicReference<Long> loanIdRef = new AtomicReference<>(); + final PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + runAt("01 March 2023", () -> { + final PostLoanProductsResponse loanProductsResponse = loanProductHelper + .createLoanProduct(create4IProgressiveWithCapitalizedIncome()); + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applyLP2ProgressiveLoanRequest(client.getClientId(), + loanProductsResponse.getResourceId(), "01 March 2023", 10000.00, 12.00, 4, null)); + Long loanId = postLoansResponse.getLoanId(); + loanIdRef.set(loanId); + + loanTransactionHelper.approveLoan(loanId, approveLoanRequest(10000.00, "01 March 2023")); + disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 March 2023"); + }); + + runAt("15 March 2023", () -> { + loanTransactionHelper.addCapitalizedIncome(loanIdRef.get(), "15 March 2023", 500.00); + loanTransactionHelper.makeLoanRepayment(loanIdRef.get(), "Repayment", "1 March 2023", 1500.00); + }); + + BigDecimal zero = BigDecimal.ZERO; + BigDecimal thousand = BigDecimal.valueOf(1000.0); + BigDecimal fiveHundred = BigDecimal.valueOf(500.0); + BigDecimal thousandFiveHundred = BigDecimal.valueOf(1500.0); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanIdRef.get()); + Assertions.assertEquals(thousand, loanDetails.getPrincipal().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(thousand, loanDetails.getSummary().getPrincipalDisbursed().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(fiveHundred, loanDetails.getSummary().getTotalCapitalizedIncome().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(thousandFiveHundred, loanDetails.getSummary().getTotalPrincipal().setScale(1, RoundingMode.HALF_UP)); + Assertions.assertEquals(zero, loanDetails.getSummary().getPrincipalOutstanding().setScale(0, RoundingMode.HALF_UP)); + } }
