You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ad...@apache.org on 2024/10/29 20:53:12 UTC
(fineract) branch develop updated: FINERACT-1981: Pay-off does not consider the overdue installments
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 f92290b35 FINERACT-1981: Pay-off does not consider the overdue installments
f92290b35 is described below
commit f92290b3553bd3d0adf9ae83f5f78a2a790ac6e1
Author: Andrii Kulminskyi <ku...@gmail.com>
AuthorDate: Tue Oct 29 13:43:12 2024 +0200
FINERACT-1981: Pay-off does not consider the overdue installments
---
.../domain/ProgressiveLoanScheduleGenerator.java | 3 +
.../domain/PrepaymentCalculationTest.java | 146 +++++++++++++++++++++
.../ClientLoanIntegrationTest.java | 101 ++++++++++++++
3 files changed, 250 insertions(+)
diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
index 411e0c71b..8cd0d8878 100644
--- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
+++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java
@@ -274,6 +274,9 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator {
.principal(result.getOutstandingBalance()) //
.interest(result.getPayableInterest());
+ installments.stream().filter(installment -> installment.getDueDate().isBefore(onDate))
+ .forEach(installment -> amounts.plusInterest(installment.getInterestOutstanding(currency)));
+
installments.forEach(installment -> amounts //
.plusFeeCharges(installment.getFeeChargesOutstanding(currency))
.plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency)));
diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrepaymentCalculationTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrepaymentCalculationTest.java
new file mode 100644
index 000000000..1fec1bb92
--- /dev/null
+++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrepaymentCalculationTest.java
@@ -0,0 +1,146 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.loanschedule.domain;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.math.MathContext;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.util.List;
+import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
+import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
+import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails;
+import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
+import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod;
+import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
+import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy;
+import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.MockitoAnnotations;
+
+public class PrepaymentCalculationTest {
+
+ private static final MockedStatic<MoneyHelper> moneyHelper = Mockito.mockStatic(MoneyHelper.class);
+ private static final MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN);
+ private static final MonetaryCurrency monetaryCurrency = MonetaryCurrency
+ .fromApplicationCurrency(new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$"));
+
+ @Mock
+ private LoanProductRelatedDetail loanProductRelatedDetail;
+
+ @Mock
+ private LoanApplicationTerms loanApplicationTerms;
+
+ @Mock
+ private AdvancedPaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor;
+
+ @Mock
+ private EMICalculator emiCalculator;
+
+ @InjectMocks
+ private ProgressiveLoanScheduleGenerator progressiveLoanScheduleGenerator;
+
+ private Loan loan;
+
+ @BeforeEach
+ public void setUp() {
+ MockitoAnnotations.openMocks(this);
+
+ moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
+ moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
+
+ loan = Mockito.mock(Loan.class);
+ when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+ when(loan.getDisbursementDate()).thenReturn(LocalDate.of(2022, 9, 7));
+ when(loan.getRepaymentScheduleInstallments()).thenReturn(createRepaymentScheduleInstallments());
+
+ when(loanApplicationTerms.getPreClosureInterestCalculationStrategy())
+ .thenReturn(LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE);
+
+ when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(BigDecimal.valueOf(7.0));
+
+ List<RepaymentPeriod> repaymentPeriods = createMockRepaymentPeriods();
+ ProgressiveLoanInterestScheduleModel scheduleModel = new ProgressiveLoanInterestScheduleModel(repaymentPeriods,
+ loanProductRelatedDetail, 100, mc);
+
+ when(loanRepaymentScheduleTransactionProcessor.reprocessProgressiveLoanTransactions(Mockito.any(), Mockito.anyList(), Mockito.any(),
+ Mockito.anyList(), Mockito.anySet())).thenReturn(org.apache.commons.lang3.tuple.Pair.of(null, scheduleModel));
+
+ PayableDetails payableDetails = new PayableDetails(Money.of(monetaryCurrency, BigDecimal.valueOf(200)),
+ Money.of(monetaryCurrency, BigDecimal.valueOf(500)), Money.of(monetaryCurrency, BigDecimal.valueOf(0)),
+ Money.of(monetaryCurrency, BigDecimal.valueOf(1000)));
+
+ when(emiCalculator.getPayableDetails(Mockito.any(ProgressiveLoanInterestScheduleModel.class), Mockito.any(LocalDate.class),
+ Mockito.any(LocalDate.class))).thenReturn(payableDetails);
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ moneyHelper.close();
+ }
+
+ @Test
+ public void testCalculatePrepaymentAmount() {
+ LocalDate prepaymentDate = LocalDate.of(2023, 6, 1);
+
+ OutstandingAmountsDTO result = progressiveLoanScheduleGenerator.calculatePrepaymentAmount(monetaryCurrency, prepaymentDate,
+ loanApplicationTerms, mc, loan, null, loanRepaymentScheduleTransactionProcessor);
+
+ assertEquals("1000.00", result.principal().getAmount().toString());
+ assertEquals("15.00", result.interest().getAmount().toString());
+ }
+
+ private List<LoanRepaymentScheduleInstallment> createRepaymentScheduleInstallments() {
+ LoanRepaymentScheduleInstallment installment1 = new LoanRepaymentScheduleInstallment(loan, 1, LocalDate.of(2022, 10, 1),
+ LocalDate.of(2022, 11, 1), BigDecimal.valueOf(500), BigDecimal.valueOf(10), BigDecimal.ZERO, BigDecimal.ZERO, false, null);
+
+ LoanRepaymentScheduleInstallment installment2 = new LoanRepaymentScheduleInstallment(loan, 2, LocalDate.of(2022, 11, 2),
+ LocalDate.of(2022, 12, 1), BigDecimal.valueOf(500), BigDecimal.valueOf(5), BigDecimal.ZERO, BigDecimal.ZERO, false, null);
+
+ return List.of(installment1, installment2);
+ }
+
+ private List<RepaymentPeriod> createMockRepaymentPeriods() {
+ RepaymentPeriod period1 = Mockito.mock(RepaymentPeriod.class);
+ when(period1.getFromDate()).thenReturn(LocalDate.of(2022, 10, 1));
+ when(period1.getDueDate()).thenReturn(LocalDate.of(2022, 11, 1));
+
+ RepaymentPeriod period2 = Mockito.mock(RepaymentPeriod.class);
+ when(period2.getFromDate()).thenReturn(LocalDate.of(2022, 11, 2));
+ when(period2.getDueDate()).thenReturn(LocalDate.of(2022, 12, 1));
+
+ return List.of(period1, period2);
+ }
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
index df133d688..a7490ce99 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java
@@ -3118,6 +3118,94 @@ public class ClientLoanIntegrationTest extends BaseLoanIntegrationTest {
}
+ @Test
+ public void testLoanPrePaymentWithMultiplePayments() {
+ final Integer clientID = ClientHelper.createClient(REQUEST_SPEC, RESPONSE_SPEC);
+ ClientHelper.verifyClientCreatedOnServer(REQUEST_SPEC, RESPONSE_SPEC, clientID);
+
+ // Create a loan product
+ Integer loanProductId = createLoanProduct(false, NONE);
+ Integer collateralId = CollateralManagementHelper.createCollateralProduct(REQUEST_SPEC, RESPONSE_SPEC);
+ Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(REQUEST_SPEC, RESPONSE_SPEC,
+ String.valueOf(clientID), collateralId);
+ List<HashMap> collaterals = List.of(collaterals(clientCollateralId, BigDecimal.ONE));
+
+ // Apply for a loan
+ final String disbursementDate = "1 May 2023";
+ final String approvalDate = "1 April 2023";
+ final String submissionDate = "1 March 2023";
+ final String interestRate = "7";
+ final Integer loanID = applyForLoanApplication(clientID, loanProductId, disbursementDate, submissionDate, interestRate, null, null,
+ "1000", collaterals);
+ Assertions.assertNotNull(loanID);
+
+ // Check loan status
+ HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(REQUEST_SPEC, RESPONSE_SPEC, loanID);
+ LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+ // Approve the loan
+ LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------");
+ loanStatusHashMap = LOAN_TRANSACTION_HELPER.approveLoan(approvalDate, loanID);
+ LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+ LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
+
+ // Disburse the loan
+ LOG.info("-------------------------------DISBURSE LOAN-------------------------------------------");
+ String loanDetails = LOAN_TRANSACTION_HELPER.getLoanDetails(REQUEST_SPEC, RESPONSE_SPEC, loanID);
+ loanStatusHashMap = LOAN_TRANSACTION_HELPER.disburseLoanWithNetDisbursalAmount(disbursementDate, loanID,
+ JsonPath.from(loanDetails).get("netDisbursalAmount").toString());
+ LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+
+ // Make the first partial repayment
+ LOG.info("------------------------MAKE FIRST PARTIAL REPAYMENT-----------------------------------");
+ Float firstRepaymentAmount = 500.0f; // First partial repayment
+ String firstRepaymentDate = "1 June 2023";
+ LOAN_TRANSACTION_HELPER.makeRepayment(firstRepaymentDate, firstRepaymentAmount, loanID);
+
+ // Verify the prepayment amount after the first partial repayment
+ LOG.info("------------------------GET PREPAYMENT AMOUNT AFTER FIRST PAYMENT-----------------------");
+ HashMap<String, Object> prepayAmount = loanTransactionHelper.getPrepayAmount(REQUEST_SPEC, RESPONSE_SPEC, loanID);
+ Assertions.assertNotNull(prepayAmount);
+
+ // Extract the principal and interest portions
+ Float totalPrepayAmount = (Float) prepayAmount.get("amount");
+ Float principalAmount = (Float) prepayAmount.get("principalPortion");
+ Float interestAmount = (Float) prepayAmount.get("interestPortion");
+
+ // Expected values after the first partial repayment
+ Float expectedTotalPrepayAmount = 606.18f;
+ Float expectedPrincipal = 570.0f;
+ Float expectedInterest = 36.18f;
+
+ // Validate calculations
+ validateNumberForEqual(String.valueOf(expectedTotalPrepayAmount), String.valueOf(totalPrepayAmount));
+ validateNumberForEqual(String.valueOf(expectedPrincipal), String.valueOf(principalAmount));
+ validateNumberForEqual(String.valueOf(expectedInterest), String.valueOf(interestAmount));
+
+ // Make the second partial repayment
+ LOG.info("------------------------MAKE SECOND PARTIAL REPAYMENT----------------------------------");
+ Float secondRepaymentAmount = 606.18f;
+ String secondRepaymentDate = "1 July 2023";
+ LOAN_TRANSACTION_HELPER.makeRepayment(secondRepaymentDate, secondRepaymentAmount, loanID);
+
+ // Recheck the prepayment amount
+ LOG.info("------------------------RECHECK PREPAYMENT AMOUNT AFTER FULL REPAYMENT------------------");
+ HashMap<String, Object> postPrepayAmount = loanTransactionHelper.getPrepayAmount(REQUEST_SPEC, RESPONSE_SPEC, loanID);
+ Assertions.assertNotNull(postPrepayAmount);
+
+ // Verify that the principal and interest portions are zero
+ Float postPrincipalAmount = (Float) postPrepayAmount.get("principalPortion");
+ Float postInterestAmount = (Float) postPrepayAmount.get("interestPortion");
+
+ validateNumberForEqual("0.0", String.valueOf(postPrincipalAmount));
+ validateNumberForEqual("0.0", String.valueOf(postInterestAmount));
+
+ // Check the loan status after repayment
+ LOG.info("------------------------CHECK LOAN STATUS---------------------------------------------");
+ loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(REQUEST_SPEC, RESPONSE_SPEC, loanID);
+ LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap);
+ }
+
@Test
public void testLoanScheduleWithInterestRecalculation_WITH_REST_SAME_AS_REPAYMENT_INTEREST_COMPOUND_NONE_STRATEGY_REDUCE_EMI() {
@@ -7217,6 +7305,19 @@ public class ClientLoanIntegrationTest extends BaseLoanIntegrationTest {
return LOAN_TRANSACTION_HELPER.getLoanId(loanApplicationJSON);
}
+ private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, String disbursementDate,
+ String submissionDate, String interestRate, List<HashMap> charges, final String savingsId, String principal,
+ List<HashMap> collaterals) {
+ LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
+ final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principal).withLoanTermFrequency("2")
+ .withLoanTermFrequencyAsMonths().withNumberOfRepayments("2").withRepaymentEveryAfter("1")
+ .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod(interestRate).withAmortizationTypeAsEqualInstallments()
+ .withInterestTypeAsDecliningBalance().withInterestCalculationPeriodTypeSameAsRepaymentPeriod()
+ .withExpectedDisbursementDate(disbursementDate).withSubmittedOnDate(submissionDate).withCollaterals(collaterals)
+ .withCharges(charges).build(clientID.toString(), loanProductID.toString(), savingsId);
+ return LOAN_TRANSACTION_HELPER.getLoanId(loanApplicationJSON);
+ }
+
private Integer applyForLoanApplicationWithExternalId(RequestSpecification requestSpecification,
ResponseSpecification responseSpecification, final Integer clientID, final Integer loanProductID, String principal,
final String externalId) {