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