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) {