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 3dd62803e6 FINERACT-2081: Fix ProgressiveEMICalculator caused infinite 
loop on AdvancedPaymentScheduleTransactionProcessor
3dd62803e6 is described below

commit 3dd62803e6be94a09a62c12e12559281e5b27988
Author: Soma Sörös <[email protected]>
AuthorDate: Tue Feb 25 11:13:15 2025 +0100

    FINERACT-2081: Fix ProgressiveEMICalculator caused infinite loop on 
AdvancedPaymentScheduleTransactionProcessor
---
 .../loanproduct/calc/ProgressiveEMICalculator.java | 21 ++++++--
 .../integrationtests/LoanInterestRefundTest.java   | 59 ++++++++++++++++++++++
 2 files changed, 76 insertions(+), 4 deletions(-)

diff --git 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
index c941569e72..b7743b549a 100644
--- 
a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
+++ 
b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java
@@ -257,10 +257,23 @@ public final class ProgressiveEMICalculator implements 
EMICalculator {
         ProgressiveLoanInterestScheduleModel recalculatedScheduleModelTillDate 
= recalculateScheduleModelTillDate(scheduleModel,
                 periodDueDate, targetDate);
         RepaymentPeriod repaymentPeriod = 
recalculatedScheduleModelTillDate.findRepaymentPeriodByDueDate(periodDueDate).orElseThrow();
-        boolean multiplePeriodIsUnpaid = 
recalculatedScheduleModelTillDate.repaymentPeriods().stream().filter(rp -> 
!rp.isFullyPaid())
-                .count() > 1L;
-        if (multiplePeriodIsUnpaid && 
!targetDate.isAfter(repaymentPeriod.getFromDate())) {
-            repaymentPeriod.setEmi(repaymentPeriod.getOriginalEmi());
+        long notFullyRepaidRepaymentPeriodCount = 
recalculatedScheduleModelTillDate.repaymentPeriods().stream()
+                .filter(rp -> !rp.isFullyPaid()).count();
+        boolean multiplePeriodIsUnpaid = notFullyRepaidRepaymentPeriodCount > 
1L;
+        boolean onePeriodIsUnpaid = notFullyRepaidRepaymentPeriodCount == 1L;
+        if (!targetDate.isAfter(repaymentPeriod.getFromDate())) {
+            if (multiplePeriodIsUnpaid) {
+                repaymentPeriod.setEmi(repaymentPeriod.getOriginalEmi());
+            } else if (repaymentPeriod.isFullyPaid() && onePeriodIsUnpaid) {
+                
repaymentPeriod.setEmi(MathUtil.min(repaymentPeriod.getOriginalEmi(), //
+                        
recalculatedScheduleModelTillDate.getTotalDuePrincipal() //
+                                
.minus(recalculatedScheduleModelTillDate.getTotalPaidPrincipal()) //
+                                
.add(recalculatedScheduleModelTillDate.getTotalDueInterest()) //
+                                
.minus(recalculatedScheduleModelTillDate.getTotalPaidInterest()) //
+                                .add(repaymentPeriod.getPaidPrincipal()) //
+                                .add(repaymentPeriod.getPaidInterest()),
+                        false)); //
+            }
         }
 
         return new PeriodDueDetails(repaymentPeriod.getEmi(), //
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java
index a39b2bd58a..40e95146dc 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java
@@ -48,6 +48,7 @@ import 
org.apache.fineract.integrationtests.common.BusinessStepHelper;
 import org.apache.fineract.integrationtests.common.ClientHelper;
 import org.apache.fineract.integrationtests.common.Utils;
 import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
 import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
@@ -1574,4 +1575,62 @@ public class LoanInterestRefundTest extends 
BaseLoanIntegrationTest {
             ); //
         });
     }
+
+    /**
+     * Goal: test Merchant issued Refund does not cause infinite loop in 
special case of 2 transaction. * interest
+     * recalculation should be on. * merchant issued refund payment allocation 
should set to Last installment * default
+     * payment allocation should set to Next Installment Make a repayment to 
repay first instalment on its due date Make
+     * MerchantIssuedRefund to fully repay almost all the installments. 2nd 
installment should be fully unpaid and 3rd
+     * installment should have less outstanding principal portion than the 
total outstanding interest on the loan ( 2nd
+     * installment ). Make a 2nd MerchantIssuedRefund equal to remaining 
principal. Verify Repayment schedules and
+     * transactions. Verify that the loan become overpaid by the amount of 2nd 
interest refund.
+     */
+    @Test
+    public void verifyMerchantIssuedRefundInTwoPortion() {
+        runAt("1 February 2025", () -> {
+            Long loanProductId = getOrCreateLoanProduct();
+            Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), 
loanProductId, "1 January 2025", 100.0, 26.0, 6, null);
+            Assertions.assertNotNull(loanId);
+            disburseLoan(loanId, BigDecimal.valueOf(100.0), "1 January 2025");
+            loanTransactionHelper.makeLoanRepayment(loanId, "Repayment", "1 
February 2025", 17.94);
+            loanTransactionHelper.makeLoanRepayment(loanId, 
"MerchantIssuedRefund", "1 February 2025", 66.41);
+            verifyTransactions(loanId, //
+                    transaction(100.0, "Disbursement", "01 January 2025"), //
+                    transaction(17.94, "Repayment", "01 February 2025"), //
+                    transaction(66.41, "Merchant Issued Refund", "01 February 
2025"), //
+                    transaction(1.47, "Interest Refund", "01 February 2025") //
+            );
+            verifyRepaymentSchedule(loanId, //
+                    installment(100.0, null, "01 January 2025"), //
+                    installment(15.73, 2.21, 0.0, true, "01 February 2025"), //
+                    installment(17.61, 0.33, 16.47, false, "01 March 2025"), //
+                    installment(12.84, 0.01, 0.26, false, "01 April 2025"), //
+                    installment(17.94, 0.0, 0.0, true, "01 May 2025"), //
+                    installment(17.94, 0.0, 0.0, true, "01 June 2025"), //
+                    installment(17.94, 0.0, 0.0, true, "01 July 2025") //
+            );
+            loanTransactionHelper.makeLoanRepayment(loanId, 
"MerchantIssuedRefund", "1 February 2025", 16.39);
+            verifyTransactions(loanId, //
+                    transaction(100.0, "Disbursement", "01 January 2025"), //
+                    transaction(17.94, "Repayment", "01 February 2025"), //
+                    transaction(66.41, "Merchant Issued Refund", "01 February 
2025"), //
+                    transaction(1.47, "Interest Refund", "01 February 2025"), 
//
+                    transaction(16.39, "Merchant Issued Refund", "01 February 
2025"), //
+                    transaction(0.36, "Interest Refund", "01 February 2025"), 
//
+                    transaction(2.21, "Accrual", "01 February 2025") //
+            );
+            verifyRepaymentSchedule(loanId, //
+                    installment(100.0, null, "01 January 2025"), //
+                    installment(15.73, 2.21, 0.0, true, "01 February 2025"), //
+                    installment(12.51, 0.0, 0.0, true, "01 March 2025"), //
+                    installment(17.94, 0.0, 0.0, true, "01 April 2025"), //
+                    installment(17.94, 0.0, 0.0, true, "01 May 2025"), //
+                    installment(17.94, 0.0, 0.0, true, "01 June 2025"), //
+                    installment(17.94, 0.0, 0.0, true, "01 July 2025") //
+            );
+            GetLoansLoanIdResponse loanDetails = 
loanTransactionHelper.getLoanDetails(loanId);
+            verifyLoanStatus(loanDetails, LoanStatus.OVERPAID);
+            Assertions.assertEquals(0.36, loanDetails.getTotalOverpaid());
+        });
+    }
 }

Reply via email to