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());
+ });
+ }
}