diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index f116b914b..375b7c51c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -2186,8 +2186,10 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener Money principalToBeScheduled = getPrincipalToBeScheduled(loanApplicationTerms); // actual outstanding balance for interest calculation Money outstandingBalance = principalToBeScheduled; + loanScheduleParams.setOutstandingBalance(outstandingBalance); // total outstanding balance as per rest for interest calculation. Money outstandingBalanceAsPerRest = outstandingBalance; + loanScheduleParams.setOutstandingBalanceAsPerRest(outstandingBalanceAsPerRest); // this is required to update total fee amounts in the // LoanScheduleModel diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index bc1b9ebca..4e9ab1562 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -70,13 +70,13 @@ public abstract class BaseLoanIntegrationTest { protected static final String DATETIME_PATTERN = "dd MMMM yyyy"; - protected final ResponseSpecification requestSpec = createResponseSpecification(200); - protected final RequestSpecification responseSpec = createRequestSpecification(); + protected final ResponseSpecification responseSpec = createResponseSpecification(200); + protected final RequestSpecification requestSpec = createRequestSpecification(); - protected final AccountHelper accountHelper = new AccountHelper(responseSpec, requestSpec); - protected final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(responseSpec, requestSpec); + protected final AccountHelper accountHelper = new AccountHelper(requestSpec, responseSpec); + protected final LoanTransactionHelper loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); protected final LoanProductHelper loanProductHelper = new LoanProductHelper(); - protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(responseSpec, requestSpec); + protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec); protected BusinessDateHelper businessDateHelper = new BusinessDateHelper(); // asset @@ -196,7 +196,7 @@ public abstract class BaseLoanIntegrationTest { protected void verifyUndoLastDisbursalShallFail(Long loanId, String expectedError) { ResponseSpecification errorResponse = new ResponseSpecBuilder().expectStatusCode(403).build(); - LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(this.responseSpec, errorResponse); + LoanTransactionHelper validationErrorHelper = new LoanTransactionHelper(this.requestSpec, errorResponse); CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, () -> { validationErrorHelper.undoLastDisbursalLoan(loanId, new PostLoansLoanIdRequest()); }); @@ -208,7 +208,7 @@ public abstract class BaseLoanIntegrationTest { } protected void verifyTransactions(Long loanId, Transaction... transactions) { - GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(responseSpec, requestSpec, loanId.intValue()); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); if (transactions == null || transactions.length == 0) { assertNull(loanDetails.getTransactions(), "No transaction is expected"); } else { @@ -241,7 +241,7 @@ public abstract class BaseLoanIntegrationTest { } protected void verifyRepaymentSchedule(Long loanId, Installment... installments) { - GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(responseSpec, requestSpec, loanId.intValue()); + GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue()); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN); Assertions.assertNotNull(loanResponse.getRepaymentSchedule()); @@ -264,25 +264,26 @@ public abstract class BaseLoanIntegrationTest { protected void runAt(String date, Runnable runnable) { try { - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(responseSpec, requestSpec, 42, true); - GlobalConfigurationHelper.updateIsBusinessDateEnabled(responseSpec, requestSpec, TRUE); + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, true); + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, TRUE); businessDateHelper.updateBusinessDate( new BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en")); runnable.run(); } finally { - GlobalConfigurationHelper.updateIsBusinessDateEnabled(responseSpec, requestSpec, FALSE); - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(responseSpec, requestSpec, 42, false); + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, FALSE); + GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(requestSpec, responseSpec, 42, false); } } - protected Long applyAndApproveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount) { + protected Long applyAndApproveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount, + int numberOfRepayments) { PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(clientId) .productId(loanProductId).expectedDisbursementDate(loanDisbursementDate).dateFormat(DATETIME_PATTERN) .transactionProcessingStrategyCode(DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY) .locale("en").submittedOnDate(loanDisbursementDate).amortizationType(1).interestRatePerPeriod(0) .interestCalculationPeriodType(1).interestType(0).repaymentFrequencyType(0).repaymentEvery(30).repaymentFrequencyType(0) - .numberOfRepayments(1).loanTermFrequency(30).loanTermFrequencyType(0).maxOutstandingLoanBalance(BigDecimal.valueOf(amount)) - .principal(BigDecimal.valueOf(amount)).loanType("individual")); + .numberOfRepayments(numberOfRepayments).loanTermFrequency(numberOfRepayments * 30).loanTermFrequencyType(0) + .maxOutstandingLoanBalance(BigDecimal.valueOf(amount)).principal(BigDecimal.valueOf(amount)).loanType("individual")); PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(amount)).dateFormat(DATETIME_PATTERN) @@ -291,6 +292,10 @@ public abstract class BaseLoanIntegrationTest { return approvedLoanResult.getLoanId(); } + protected Long applyAndApproveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount) { + return applyAndApproveLoan(clientId, loanProductId, loanDisbursementDate, amount, 1); + } + protected void addRepaymentForLoan(Long loanId, Double amount, String date) { String firstRepaymentUUID = UUID.randomUUID().toString(); loanTransactionHelper.makeLoanRepayment(loanId, new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN) diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleTestWithDownpayment.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleTestWithDownpayment.java new file mode 100644 index 000000000..e1567eafc --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleTestWithDownpayment.java @@ -0,0 +1,138 @@ +/** + * 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 static java.lang.Boolean.TRUE; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.math.BigDecimal; +import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper; +import org.apache.fineract.integrationtests.common.loans.LoanRescheduleRequestTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@ExtendWith(LoanTestLifecycleExtension.class) +public class LoanRescheduleTestWithDownpayment extends BaseLoanIntegrationTest { + + public static final BigDecimal DOWN_PAYMENT_PERCENTAGE = new BigDecimal(25); + private final ClientHelper clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); + + private final LoanRescheduleRequestHelper loanRescheduleRequestHelper = new LoanRescheduleRequestHelper(this.requestSpec, + this.responseSpec); + + @Test + public void testRescheduleWithDownpayment() { + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + // Create Loan Product + Long loanProductId = createLoanProductWith25PctDownPayment(true, true); + + // Apply and Approve Loan + Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 1500.0, 2); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanId, // + installment(1500.0, null, "01 January 2023"), // + installment(375.0, false, "01 January 2023"), // + installment(563.0, false, "31 January 2023"), // + installment(562.0, false, "02 March 2023") // + ); + + // 1st Disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January 2023"); + + // verify transactions + verifyTransactions(loanId, // + transaction(250.0, "Down Payment", "01 January 2023"), // + transaction(1000.0, "Disbursement", "01 January 2023") // + ); + + // verify journal entries + verifyJournalEntries(loanId, journalEntry(250.0, loansReceivableAccount, "CREDIT"), // + journalEntry(250.0, suspenseClearingAccount, "DEBIT"), // + journalEntry(1000.0, loansReceivableAccount, "DEBIT"), // + journalEntry(1000.0, suspenseClearingAccount, "CREDIT") // + ); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanId, // + installment(1000.0, null, "01 January 2023"), // + installment(250.0, true, "01 January 2023"), // + installment(375.0, false, "31 January 2023"), // + installment(375.0, false, "02 March 2023") // + ); + + String requestJSON = new LoanRescheduleRequestTestBuilder().updateGraceOnInterest(null).updateGraceOnPrincipal(null) + .updateExtraTerms(null).updateNewInterestRate(null).updateRescheduleFromDate("31 January 2023") + .updateAdjustedDueDate("15 February 2023").updateSubmittedOnDate("01 January 2023").updateRescheduleReasonId("1") + .build(loanId.toString()); + + Integer loanRescheduleRequest = loanRescheduleRequestHelper.createLoanRescheduleRequest(requestJSON); + requestJSON = new LoanRescheduleRequestTestBuilder().updateSubmittedOnDate("01 January 2023") + .getApproveLoanRescheduleRequestJSON(); + Integer approveLoanRescheduleRequest = loanRescheduleRequestHelper.approveLoanRescheduleRequest(loanRescheduleRequest, + requestJSON); + + verifyRepaymentSchedule(loanId, // + installment(1000.0, null, "01 January 2023"), // + installment(250.0, true, "01 January 2023"), // + installment(375.0, false, "15 February 2023"), // + installment(375.0, false, "17 March 2023") // + ); + }); + } + + private Long createLoanProductWith25PctDownPayment(boolean autoDownPaymentEnabled, boolean multiDisburseEnabled) { + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct(); + product.setMultiDisburseLoan(multiDisburseEnabled); + + if (!multiDisburseEnabled) { + product.disallowExpectedDisbursements(null); + product.setAllowApprovedDisbursedAmountsOverApplied(null); + product.overAppliedCalculationType(null); + product.overAppliedNumber(null); + } + + product.setEnableDownPayment(true); + product.setDisbursedAmountPercentageForDownPayment(DOWN_PAYMENT_PERCENTAGE); + product.setEnableAutoRepaymentForDownPayment(autoDownPaymentEnabled); + + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + GetLoanProductsProductIdResponse getLoanProductsProductIdResponse = loanProductHelper + .retrieveLoanProductById(loanProductResponse.getResourceId()); + + Long loanProductId = loanProductResponse.getResourceId(); + + assertEquals(TRUE, getLoanProductsProductIdResponse.getEnableDownPayment()); + assertNotNull(getLoanProductsProductIdResponse.getDisbursedAmountPercentageForDownPayment()); + assertEquals(0, getLoanProductsProductIdResponse.getDisbursedAmountPercentageForDownPayment().compareTo(DOWN_PAYMENT_PERCENTAGE)); + assertEquals(autoDownPaymentEnabled, getLoanProductsProductIdResponse.getEnableAutoRepaymentForDownPayment()); + assertEquals(multiDisburseEnabled, getLoanProductsProductIdResponse.getMultiDisburseLoan()); + return loanProductId; + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoLoanDisbursalWithDownPaymentIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoLoanDisbursalWithDownPaymentIntegrationTest.java index 8e67f7395..9091ba30d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoLoanDisbursalWithDownPaymentIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoLoanDisbursalWithDownPaymentIntegrationTest.java @@ -37,7 +37,7 @@ import org.junit.jupiter.api.extension.ExtendWith; public class UndoLoanDisbursalWithDownPaymentIntegrationTest extends BaseLoanIntegrationTest { public static final BigDecimal DOWN_PAYMENT_PERCENTAGE = new BigDecimal(25); - private final ClientHelper clientHelper = new ClientHelper(this.responseSpec, this.requestSpec); + private final ClientHelper clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); @Test public void testUndoDisbursalForLoanWithSingleDisbursalAutoDownPaymentEnabledAndNoManualTransactions() {