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 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 getAccountMappingForCashBased() { final Map 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);