You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@fineract.apache.org by ar...@apache.org on 2022/05/16 16:56:31 UTC

[fineract] branch develop updated: Update: Cater for loans with no schedule at disburse time

This is an automated email from the ASF dual-hosted git repository.

arnold 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 c1a3b98ed Update: Cater for loans with no schedule at disburse time
c1a3b98ed is described below

commit c1a3b98edd5670c5bdfc7bb2295a1ca4877a35b2
Author: John Woodlock <jo...@gmail.com>
AuthorDate: Mon May 16 12:17:46 2022 +0100

    Update: Cater for loans with no schedule at disburse time
---
 .../portfolio/loanaccount/domain/Loan.java         |  13 +-
 .../LoanWritePlatformServiceJpaRepositoryImpl.java |   6 +
 ...ntLoanMultipleDisbursementsIntegrationTest.java |   4 -
 ...rancheMultipleDisbursementsIntegrationTest.java | 206 +++++++++++++++++++++
 .../common/loans/LoanApplicationTestBuilder.java   |   3 +-
 .../common/loans/LoanProductTestBuilder.java       |  31 ++++
 .../common/loans/LoanTransactionHelper.java        |   4 +-
 7 files changed, 249 insertions(+), 18 deletions(-)

diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index 3302a9270..9dd9a5c00 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -2481,17 +2481,8 @@ public class Loan extends AbstractPersistableCustom {
 
     private void regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(boolean interestRecalculationEnabledParam,
             boolean disbursementMissedParam, ScheduleGeneratorDTO scheduleGeneratorDTO, AppUser currentUser) {
-        /*
-         * There may be no schedule built pre-disbursal e.g. multi-disbursal products that disallow expected
-         * disbursements
-         */
-        LocalDate firstInstallmentDueDate = null;
-        LoanRepaymentScheduleInstallment firstInstallment = fetchRepaymentScheduleInstallment(1);
-        if (firstInstallment == null) {
-            firstInstallmentDueDate = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());
-        } else {
-            firstInstallmentDueDate = firstInstallment.getDueDate();
-        }
+
+        LocalDate firstInstallmentDueDate = fetchRepaymentScheduleInstallment(1).getDueDate();
         if (interestRecalculationEnabledParam
                 && (firstInstallmentDueDate.isBefore(LocalDate.now(DateUtils.getDateTimeZoneOfTenant())) || disbursementMissedParam)) {
             regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO, currentUser);
diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
index e1493b608..8e0dd45cf 100644
--- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
+++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
@@ -513,6 +513,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
                 loan.addLoanTransaction(disbursementTransaction);
             }
 
+            if (loan.getRepaymentScheduleInstallments().size() == 0) {
+                /*
+                 * If no schedule, generate one (applicable to non-tranche multi-disbursal loans)
+                 */
+                recalculateSchedule = true;
+            }
             regenerateScheduleOnDisbursement(command, loan, recalculateSchedule, scheduleGeneratorDTO, nextPossibleRepaymentDate,
                     rescheduledRepaymentDate);
             if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanMultipleDisbursementsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanMultipleDisbursementsIntegrationTest.java
index aa40781a4..e50b79061 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanMultipleDisbursementsIntegrationTest.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanMultipleDisbursementsIntegrationTest.java
@@ -50,9 +50,6 @@ public class ClientLoanMultipleDisbursementsIntegrationTest {
 
     private static final Logger LOG = LoggerFactory.getLogger(ClientLoanMultipleDisbursementsIntegrationTest.class);
 
-    public static final String MINIMUM_OPENING_BALANCE = "1000.0";
-    public static final String ACCOUNT_TYPE_INDIVIDUAL = "INDIVIDUAL";
-
     private ResponseSpecification responseSpec;
     private RequestSpecification requestSpec;
     private LoanTransactionHelper loanTransactionHelper;
@@ -163,7 +160,6 @@ public class ClientLoanMultipleDisbursementsIntegrationTest {
         LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
 
         LOG.info("-------------------------------DISBURSE 8 LOANS -------------------------------------------");
-        // String loanDetails = this.loanTransactionHelper.getLoanDetails(this.requestSpec, this.responseSpec, loanID);
         loanStatusHashMap = this.loanTransactionHelper.disburseLoan("12 January 2021", loanID, "1");
         LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
         loanStatusHashMap = this.loanTransactionHelper.disburseLoan("12 January 2021", loanID, "2");
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanNonTrancheMultipleDisbursementsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanNonTrancheMultipleDisbursementsIntegrationTest.java
new file mode 100644
index 000000000..fecca0c2b
--- /dev/null
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanNonTrancheMultipleDisbursementsIntegrationTest.java
@@ -0,0 +1,206 @@
+/**
+ * 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.integrationtests;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.util.ArrayList;
+import java.util.HashMap;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker;
+import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Client Loan Integration Test for checking Loan Application Repayments Schedule, loan charges, penalties, loan
+ * repayments and verifying accounting transactions
+ */
+@SuppressWarnings({ "rawtypes", "unchecked" })
+public class ClientLoanNonTrancheMultipleDisbursementsIntegrationTest {
+
+    private static final Logger LOG = LoggerFactory.getLogger(ClientLoanNonTrancheMultipleDisbursementsIntegrationTest.class);
+
+    private static final String APPLIED_FOR_PRINCIPAL = "12,000.0";
+
+    private ResponseSpecification responseSpec;
+    private RequestSpecification requestSpec;
+    private LoanTransactionHelper loanTransactionHelper;
+
+    @BeforeEach
+    public void setup() {
+        Utils.initializeRESTAssured();
+        this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
+        this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+        this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+        this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
+    }
+
+    private Integer createLoanProduct(final boolean isInterestRecalculationEnabled) {
+        LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------");
+        LoanProductTestBuilder builder = new LoanProductTestBuilder() //
+                .withPrincipal(APPLIED_FOR_PRINCIPAL) //
+                .withNumberOfRepayments("4") //
+                .withRepaymentAfterEvery("1") //
+                .withRepaymentTypeAsMonth() //
+                .withinterestRatePerPeriod("1") //
+                .withInterestRateFrequencyTypeAsMonths() //
+                .withAmortizationTypeAsEqualInstallments() //
+                .withInterestTypeAsDecliningBalance() //
+                .withMultiDisburse() //
+                .withInterestCalculationPeriodTypeAsRepaymentPeriod(true) //
+                .withMaxTrancheCount("30") //
+                .withDisallowExpectectedDisbursements(true);
+        if (isInterestRecalculationEnabled) {
+            final String interestRecalculationCompoundingMethod = LoanProductTestBuilder.RECALCULATION_COMPOUNDING_METHOD_NONE;
+            final String rescheduleStrategyMethod = LoanProductTestBuilder.RECALCULATION_STRATEGY_REDUCE_NUMBER_OF_INSTALLMENTS;
+            final String recalculationRestFrequencyType = LoanProductTestBuilder.RECALCULATION_FREQUENCY_TYPE_DAILY;
+            final String recalculationRestFrequencyInterval = "0";
+            final String preCloseInterestCalculationStrategy = LoanProductTestBuilder.INTEREST_APPLICABLE_STRATEGY_ON_PRE_CLOSE_DATE;
+            final String recalculationCompoundingFrequencyType = null;
+            final String recalculationCompoundingFrequencyInterval = null;
+            final Integer recalculationCompoundingFrequencyOnDayType = null;
+            final Integer recalculationCompoundingFrequencyDayOfWeekType = null;
+            final Integer recalculationRestFrequencyOnDayType = null;
+            final Integer recalculationRestFrequencyDayOfWeekType = null;
+            builder = builder
+                    .withInterestRecalculationDetails(interestRecalculationCompoundingMethod, rescheduleStrategyMethod,
+                            preCloseInterestCalculationStrategy)
+                    .withInterestRecalculationRestFrequencyDetails(recalculationRestFrequencyType, recalculationRestFrequencyInterval,
+                            recalculationRestFrequencyOnDayType, recalculationRestFrequencyDayOfWeekType)
+                    .withInterestRecalculationCompoundingFrequencyDetails(recalculationCompoundingFrequencyType,
+                            recalculationCompoundingFrequencyInterval, recalculationCompoundingFrequencyOnDayType,
+                            recalculationCompoundingFrequencyDayOfWeekType);
+        }
+        final String loanProductJSON = builder.build(null);
+        return this.loanTransactionHelper.getLoanProductId(loanProductJSON);
+    }
+
+    private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, final String savingsId, String principal,
+            String submitDate, String repaymentsNo) {
+        LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------");
+        final String loanApplicationJSON = new LoanApplicationTestBuilder() //
+                .withPrincipal(principal) //
+                .withLoanTermFrequency(repaymentsNo) //
+                .withLoanTermFrequencyAsMonths() //
+                .withNumberOfRepayments(repaymentsNo) //
+                .withRepaymentEveryAfter("1") //
+                .withRepaymentFrequencyTypeAsMonths() //
+                .withInterestRatePerPeriod("2") //
+                .withAmortizationTypeAsEqualInstallments() //
+                .withInterestTypeAsDecliningBalance() //
+                .withInterestCalculationPeriodTypeSameAsRepaymentPeriod() //
+                .withExpectedDisbursementDate(submitDate) //
+                .withTranches(null) //
+                .withSubmittedOnDate(submitDate) //
+                .build(clientID.toString(), loanProductID.toString(), savingsId);
+        return this.loanTransactionHelper.getLoanId(loanApplicationJSON);
+    }
+
+    /***
+     * Defensive Test case to ensure that the first disbursal for a non-tranche multi-disbursal loan creates a schedule
+     */
+    @Test
+    public void checkThatNonTrancheMultiDisbursalsCreateAScheduleOnFirstDisbursalTest() {
+        this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec);
+
+        final Integer clientID = ClientHelper.createClient(this.requestSpec, this.responseSpec);
+        ClientHelper.verifyClientCreatedOnServer(this.requestSpec, this.responseSpec, clientID);
+
+        /***
+         * Create loan product allowing non-tranche multiple disbursals with interest recalculation
+         */
+        boolean isInterestRecalculationEnabled = true;
+        final Integer loanProductID = createLoanProduct(isInterestRecalculationEnabled);
+        Assertions.assertNotNull(loanProductID);
+
+        /***
+         * Apply for loan application and verify loan status
+         */
+        final String savingsId = null;
+        String submitDate = "01 January 2021";
+        Integer repaymentsNo = 3;
+        final Integer loanID = applyForLoanApplication(clientID, loanProductID, savingsId, APPLIED_FOR_PRINCIPAL, submitDate,
+                repaymentsNo.toString());
+        Assertions.assertNotNull(loanID);
+        HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
+        LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap);
+
+        LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------");
+        final Float approved = 9000.00f;
+        loanStatusHashMap = this.loanTransactionHelper.approveLoanWithApproveAmount(submitDate, null, approved.toString(), loanID, null);
+        LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap);
+        LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap);
+        ArrayList<HashMap> loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
+
+        LOG.info("-------------------------------DISBURSE non-tranch multi-disbursal loan       ----------");
+        final String netDisbursedAmt = null;
+        loanStatusHashMap = this.loanTransactionHelper.disburseLoan(submitDate, loanID, approved.toString(), netDisbursedAmt);
+
+        LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+        loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
+        Integer loanScheduleLineCount = loanSchedule.size() - 1;
+        Assertions.assertEquals(repaymentsNo, loanScheduleLineCount);
+
+        HashMap loanSummary = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID);
+        Assertions.assertEquals(approved, loanSummary.get("principalDisbursed"));
+        Assertions.assertEquals(approved, loanSummary.get("principalOutstanding"));
+
+        LOG.info("------------------------------- 2nd DISBURSE non-tranch multi-disbursal loan       ----------");
+        final Float anotherDisbursalAmount = 900.00f;
+        loanStatusHashMap = this.loanTransactionHelper.disburseLoan(submitDate, loanID, anotherDisbursalAmount.toString(), netDisbursedAmt);
+
+        LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+        loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
+        loanScheduleLineCount = loanSchedule.size() - 2;
+        Assertions.assertEquals(repaymentsNo, loanScheduleLineCount);
+
+        loanSummary = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID);
+        Float disbursedSum = approved + anotherDisbursalAmount;
+        Assertions.assertEquals(disbursedSum, loanSummary.get("principalDisbursed"));
+        Assertions.assertEquals(disbursedSum, loanSummary.get("principalOutstanding"));
+
+        LOG.info("------------------------------- 3rd DISBURSE non-tranch multi-disbursal loan       ----------");
+        final Float thirdDisbursalAmount = 500.00f;
+        String thirdDisbursalDate = "03 February 2021";
+        loanStatusHashMap = this.loanTransactionHelper.disburseLoan(thirdDisbursalDate, loanID, thirdDisbursalAmount.toString(),
+                netDisbursedAmt);
+
+        LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap);
+        loanSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(this.requestSpec, this.responseSpec, loanID);
+        loanScheduleLineCount = loanSchedule.size() - 3;
+        Assertions.assertEquals(repaymentsNo, loanScheduleLineCount);
+
+        loanSummary = this.loanTransactionHelper.getLoanSummary(this.requestSpec, this.responseSpec, loanID);
+        disbursedSum = disbursedSum + thirdDisbursalAmount;
+        Assertions.assertEquals(disbursedSum, loanSummary.get("principalDisbursed"));
+        Assertions.assertEquals(disbursedSum, loanSummary.get("principalOutstanding"));
+
+    }
+
+}
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java
index 06fb68acf..76f0664a8 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanApplicationTestBuilder.java
@@ -175,9 +175,8 @@ public class LoanApplicationTestBuilder {
         if (disbursementData != null) {
             map.put("disbursementData", disbursementData);
             map.put("fixedEmiAmount", fixedEmiAmount);
-            map.put("maxOutstandingLoanBalance", maxOutstandingLoanBalance);
-
         }
+        map.put("maxOutstandingLoanBalance", maxOutstandingLoanBalance);
 
         if (datatables != null) {
             map.put("datatables", this.datatables);
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
index 7f0e0f8b8..593a0d7dc 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductTestBuilder.java
@@ -94,6 +94,10 @@ public class LoanProductTestBuilder {
     private Boolean multiDisburseLoan = false;
     private final String outstandingLoanBalance = "35000";
     private String maxTrancheCount = "3";
+    private Boolean disallowExpectedDisbursements = false;
+    private Boolean allowApprovedDisbursedAmountsOverApplied = false;
+    private String overAppliedCalculationType = null;
+    private Integer overAppliedNumber = null;
 
     private Boolean isInterestRecalculationEnabled = false;
     private String daysInYearType = "1";
@@ -163,6 +167,18 @@ public class LoanProductTestBuilder {
         if (this.minimumDaysBetweenDisbursalAndFirstRepayment != null) {
             map.put("minimumDaysBetweenDisbursalAndFirstRepayment", this.minimumDaysBetweenDisbursalAndFirstRepayment);
         }
+        if (this.multiDisburseLoan) {
+            map.put("multiDisburseLoan", this.multiDisburseLoan);
+            map.put("maxTrancheCount", this.maxTrancheCount);
+            map.put("outstandingLoanBalance", this.outstandingLoanBalance);
+            map.put("disallowExpectedDisbursements", this.disallowExpectedDisbursements);
+            if (this.disallowExpectedDisbursements) {
+                map.put("allowApprovedDisbursedAmountsOverApplied", this.allowApprovedDisbursedAmountsOverApplied);
+                map.put("overAppliedCalculationType", this.overAppliedCalculationType);
+                map.put("overAppliedNumber", this.overAppliedNumber);
+            }
+        }
+
         if (multiDisburseLoan) {
             map.put("multiDisburseLoan", this.multiDisburseLoan);
             map.put("maxTrancheCount", this.maxTrancheCount);
@@ -355,6 +371,21 @@ public class LoanProductTestBuilder {
         return this;
     }
 
+    public LoanProductTestBuilder withMultiDisburse() {
+        this.multiDisburseLoan = true;
+        return this;
+    }
+
+    public LoanProductTestBuilder withDisallowExpectectedDisbursements(boolean disallowExpectectedDisbursements) {
+        this.disallowExpectedDisbursements = disallowExpectectedDisbursements;
+        if (this.disallowExpectedDisbursements) {
+            this.allowApprovedDisbursedAmountsOverApplied = true;
+            this.overAppliedCalculationType = "percentage";
+            this.overAppliedNumber = 100;
+        }
+        return this;
+    }
+
     private Map<String, String> getAccountMappingForCashBased() {
         final Map<String, String> map = new HashMap<>();
         for (int i = 0; i < this.accountList.length; i++) {
diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index 85527944d..ad8626a29 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -456,7 +456,9 @@ public class LoanTransactionHelper {
         map.put("locale", "en");
         map.put("dateFormat", "dd MMMM yyyy");
         map.put("actualDisbursementDate", actualDisbursementDate);
-        map.put("netDisbursalAmount", netDisbursalAmount);
+        if (netDisbursalAmount != null) {
+            map.put("netDisbursalAmount", netDisbursalAmount);
+        }
         map.put("note", "DISBURSE NOTE");
         if (transactionAmount != null) {
             map.put("transactionAmount", transactionAmount);