FINERACT-2107: Allow interest rate change on fully paid installment for Progressive loans
This commit is contained in:
+4
@@ -19,7 +19,9 @@
|
||||
package org.apache.fineract.portfolio.loanaccount.rescheduleloan.data;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
|
||||
|
||||
public interface LoanRescheduleRequestDataValidator {
|
||||
@@ -29,4 +31,6 @@ public interface LoanRescheduleRequestDataValidator {
|
||||
void validateForApproveAction(JsonCommand jsonCommand, LoanRescheduleRequest loanRescheduleRequest);
|
||||
|
||||
void validateForRejectAction(JsonCommand jsonCommand, LoanRescheduleRequest loanRescheduleRequest);
|
||||
|
||||
void validateReschedulingInstallment(DataValidatorBuilder dataValidatorBuilder, LoanRepaymentScheduleInstallment installment);
|
||||
}
|
||||
|
||||
+2
-2
@@ -199,8 +199,8 @@ public class LoanRescheduleRequestDataValidatorImpl implements LoanRescheduleReq
|
||||
jsonCommand.checkForUnsupportedParameters(typeToken, jsonString, createRequestDataParameters);
|
||||
}
|
||||
|
||||
public static void validateReschedulingInstallment(DataValidatorBuilder dataValidatorBuilder,
|
||||
LoanRepaymentScheduleInstallment installment) {
|
||||
@Override
|
||||
public void validateReschedulingInstallment(DataValidatorBuilder dataValidatorBuilder, LoanRepaymentScheduleInstallment installment) {
|
||||
if (installment == null) {
|
||||
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName)
|
||||
.failWithCode("repayment.schedule.installment.does.not.exist", "Repayment schedule installment does not exist");
|
||||
|
||||
+8
-1
@@ -29,7 +29,6 @@ import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.Loan
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonComment;
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleReasonId;
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateRescheduleRequestStatus;
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateReschedulingInstallment;
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateSubmittedOnDate;
|
||||
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateSupportedParameters;
|
||||
|
||||
@@ -120,6 +119,14 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateReschedulingInstallment(DataValidatorBuilder dataValidatorBuilder, LoanRepaymentScheduleInstallment installment) {
|
||||
if (installment == null) {
|
||||
dataValidatorBuilder.reset().parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName)
|
||||
.failWithCode("repayment.schedule.installment.does.not.exist", "Repayment schedule installment does not exist");
|
||||
}
|
||||
}
|
||||
|
||||
private void validateUnsupportedParams(JsonElement jsonElement, DataValidatorBuilder dataValidatorBuilder) {
|
||||
final var unsupportedFields = List.of(RescheduleLoansApiConstants.graceOnPrincipalParamName, //
|
||||
RescheduleLoansApiConstants.graceOnInterestParamName, //
|
||||
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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.rescheduleloan.data;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
|
||||
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansApiConstants;
|
||||
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class ProgressiveLoanRescheduleRequestDataValidatorTest {
|
||||
|
||||
@Mock
|
||||
private DataValidatorBuilder dataValidatorBuilder;
|
||||
|
||||
@Mock
|
||||
private LoanRepaymentScheduleInstallment installment;
|
||||
|
||||
@Mock
|
||||
private FromJsonHelper fromJsonHelper;
|
||||
|
||||
@Mock
|
||||
private LoanRescheduleRequestRepository loanRescheduleRequestRepository;
|
||||
|
||||
private ProgressiveLoanRescheduleRequestDataValidator progressiveLoanRescheduleRequestDataValidator;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
progressiveLoanRescheduleRequestDataValidator = new ProgressiveLoanRescheduleRequestDataValidator(fromJsonHelper,
|
||||
loanRescheduleRequestRepository);
|
||||
when(dataValidatorBuilder.reset()).thenReturn(dataValidatorBuilder);
|
||||
when(dataValidatorBuilder.parameter(anyString())).thenReturn(dataValidatorBuilder);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailWhenInstallmentIsNull() {
|
||||
progressiveLoanRescheduleRequestDataValidator.validateReschedulingInstallment(dataValidatorBuilder, null);
|
||||
|
||||
verify(dataValidatorBuilder).reset();
|
||||
verify(dataValidatorBuilder).parameter(RescheduleLoansApiConstants.rescheduleFromDateParamName);
|
||||
verify(dataValidatorBuilder).failWithCode("repayment.schedule.installment.does.not.exist",
|
||||
"Repayment schedule installment does not exist");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotFailWhenInstallmentIsNotNull() {
|
||||
assertDoesNotThrow(
|
||||
() -> progressiveLoanRescheduleRequestDataValidator.validateReschedulingInstallment(dataValidatorBuilder, installment));
|
||||
|
||||
verify(dataValidatorBuilder, never()).failWithCode(anyString(), anyString());
|
||||
}
|
||||
}
|
||||
+30
@@ -23,6 +23,7 @@ import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDat
|
||||
import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.TransactionProcessingStrategyCode.ADVANCED_PAYMENT_ALLOCATION_STRATEGY;
|
||||
import static org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
@@ -714,6 +715,35 @@ public abstract class BaseLoanIntegrationTest {
|
||||
return chargeId.longValue();
|
||||
}
|
||||
|
||||
protected void verifyRepaymentSchedule(GetLoansLoanIdResponse savedLoanResponse, GetLoansLoanIdResponse actualLoanResponse,
|
||||
int totalPeriods, int identicalPeriods) {
|
||||
List<GetLoansLoanIdRepaymentPeriod> savedPeriods = savedLoanResponse.getRepaymentSchedule().getPeriods();
|
||||
List<GetLoansLoanIdRepaymentPeriod> actualPeriods = actualLoanResponse.getRepaymentSchedule().getPeriods();
|
||||
|
||||
assertEquals(totalPeriods, savedPeriods.size(), "Unexpected number of periods in savedPeriods list.");
|
||||
assertEquals(totalPeriods, actualPeriods.size(), "Unexpected number of periods in actualPeriods list.");
|
||||
|
||||
verifyPeriodsEquality(savedPeriods, actualPeriods, 0, identicalPeriods, true);
|
||||
|
||||
verifyPeriodsEquality(savedPeriods, actualPeriods, identicalPeriods, totalPeriods, false);
|
||||
}
|
||||
|
||||
private void verifyPeriodsEquality(List<GetLoansLoanIdRepaymentPeriod> savedPeriods, List<GetLoansLoanIdRepaymentPeriod> actualPeriods,
|
||||
int startIndex, int endIndex, boolean shouldEqual) {
|
||||
for (int i = startIndex; i < endIndex; i++) {
|
||||
Double savedTotalDue = savedPeriods.get(i).getTotalDueForPeriod();
|
||||
Double actualTotalDue = actualPeriods.get(i).getTotalDueForPeriod();
|
||||
|
||||
if (shouldEqual) {
|
||||
assertEquals(savedTotalDue, actualTotalDue, String.format(
|
||||
"Period %d should be identical in both responses. Expected: %s, Actual: %s", i + 1, savedTotalDue, actualTotalDue));
|
||||
} else {
|
||||
assertNotEquals(savedTotalDue, actualTotalDue, String
|
||||
.format("Period %d should differ between responses. Saved: %s, Actual: %s", i + 1, savedTotalDue, actualTotalDue));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void verifyRepaymentSchedule(Long loanId, Installment... installments) {
|
||||
GetLoansLoanIdResponse loanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
|
||||
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
|
||||
|
||||
+84
@@ -34,6 +34,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import org.apache.fineract.client.models.AdvancedPaymentData;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.PostClientsResponse;
|
||||
import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest;
|
||||
import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse;
|
||||
@@ -369,6 +370,89 @@ public class LoanRescheduleRequestTest extends BaseLoanIntegrationTest {
|
||||
this.createLoanRescheduleChangeEMIRequest();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void givenProgressiveLoanWithPaidInstallmentWhenInterestRateChangedThenDueAmountUpdated() {
|
||||
PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
||||
Integer commonLoanProductId = createProgressiveLoanProduct();
|
||||
|
||||
AtomicReference<PostLoansResponse> loanResponse = new AtomicReference<>();
|
||||
runAt("2 February 2024", () -> {
|
||||
loanResponse
|
||||
.set(applyForLoanWithRecalculation(client.getClientId(), commonLoanProductId, "01 January 2024", "01 January 2024"));
|
||||
|
||||
approveAndDisburseLoan(loanResponse.get().getLoanId(), "01 January 2024", BigDecimal.valueOf(100));
|
||||
makeRepayments(loanResponse.get().getLoanId().intValue());
|
||||
|
||||
GetLoansLoanIdResponse savedLoanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec,
|
||||
loanResponse.get().getLoanId().intValue());
|
||||
|
||||
PostCreateRescheduleLoansResponse rescheduleLoansResponse = rescheduleLoanWithNewInterestRate(loanResponse.get().getLoanId(),
|
||||
"2 February 2024", BigDecimal.ONE, "3 February 2024");
|
||||
|
||||
approveLoanReschedule(rescheduleLoansResponse.getResourceId(), "2 February 2024");
|
||||
|
||||
GetLoansLoanIdResponse actualLoanResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec,
|
||||
loanResponse.get().getLoanId().intValue());
|
||||
|
||||
verifyRepaymentSchedule(savedLoanResponse, actualLoanResponse, 7, 3);
|
||||
});
|
||||
}
|
||||
|
||||
private Integer createProgressiveLoanProduct() {
|
||||
AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation("NEXT_INSTALLMENT");
|
||||
final String loanProductJSON = new LoanProductTestBuilder().withNumberOfRepayments(numberOfRepayments)
|
||||
.withinterestRatePerPeriod("7").withMaxTrancheCount("10").withMinPrincipal("1").withPrincipal("100")
|
||||
.withInterestRateFrequencyTypeAsYear()
|
||||
.withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
|
||||
.withInterestTypeAsDecliningBalance().addAdvancedPaymentAllocation(defaultAllocation)
|
||||
.withInterestRecalculationDetails("0", "4", "1").withRecalculationRestFrequencyType("2")
|
||||
.withInterestCalculationPeriodTypeAsDays().withMultiDisburse().withDisallowExpectedDisbursements()
|
||||
.withLoanScheduleType(LoanScheduleType.PROGRESSIVE).withLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL)
|
||||
.build(null);
|
||||
return loanTransactionHelper.getLoanProductId(loanProductJSON);
|
||||
}
|
||||
|
||||
private PostLoansResponse applyForLoanWithRecalculation(Long clientId, Integer loanProductId, String expectedDisbursementDate,
|
||||
String submittedOnDate) {
|
||||
return loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(clientId).productId(loanProductId.longValue())
|
||||
.expectedDisbursementDate(expectedDisbursementDate).dateFormat(DATETIME_PATTERN)
|
||||
.transactionProcessingStrategyCode(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
|
||||
.locale("en").submittedOnDate(submittedOnDate).amortizationType(1).interestRatePerPeriod(BigDecimal.valueOf(7))
|
||||
.interestCalculationPeriodType(0).interestType(0).repaymentFrequencyType(2).repaymentEvery(1).numberOfRepayments(6)
|
||||
.loanTermFrequency(6).loanTermFrequencyType(2).principal(BigDecimal.valueOf(100)).loanType("individual"));
|
||||
}
|
||||
|
||||
private void approveAndDisburseLoan(Long loanId, String date, BigDecimal amount) {
|
||||
loanTransactionHelper.approveLoan(loanId, createLoanApprovalRequest(date, amount));
|
||||
loanTransactionHelper.disburseLoan(loanId, createDisbursementRequest(date, amount));
|
||||
}
|
||||
|
||||
private PostLoansLoanIdRequest createLoanApprovalRequest(String date, BigDecimal amount) {
|
||||
return new PostLoansLoanIdRequest().approvedLoanAmount(amount).dateFormat(DATETIME_PATTERN).approvedOnDate(date).locale("en");
|
||||
}
|
||||
|
||||
private PostLoansLoanIdRequest createDisbursementRequest(String date, BigDecimal amount) {
|
||||
return new PostLoansLoanIdRequest().actualDisbursementDate(date).dateFormat(DATETIME_PATTERN).transactionAmount(amount)
|
||||
.locale("en");
|
||||
}
|
||||
|
||||
private void makeRepayments(int loanId) {
|
||||
loanTransactionHelper.makeRepayment("01 February 2024", 17.01f, loanId);
|
||||
loanTransactionHelper.makeRepayment("02 February 2024", 17.01f, loanId);
|
||||
}
|
||||
|
||||
private PostCreateRescheduleLoansResponse rescheduleLoanWithNewInterestRate(Long loanId, String submittedOnDate,
|
||||
BigDecimal newInterestRate, String rescheduleFromDate) {
|
||||
return loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanId)
|
||||
.dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate(submittedOnDate).newInterestRate(newInterestRate)
|
||||
.rescheduleReasonId(1L).rescheduleFromDate(rescheduleFromDate));
|
||||
}
|
||||
|
||||
private void approveLoanReschedule(Long rescheduleId, String approvedOnDate) {
|
||||
loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleId,
|
||||
new PostUpdateRescheduleLoansRequest().approvedOnDate(approvedOnDate).locale("en").dateFormat(DATETIME_PATTERN));
|
||||
}
|
||||
|
||||
private PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final BigDecimal principal,
|
||||
final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final BigDecimal interestRate,
|
||||
final String expectedDisbursementDate, final String submittedOnDate, String transactionProcessorCode,
|
||||
|
||||
+10
@@ -497,6 +497,11 @@ public class LoanProductTestBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoanProductTestBuilder withDisallowExpectedDisbursements() {
|
||||
this.disallowExpectedDisbursements = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoanProductTestBuilder withFullAccountingConfig(String accountingRule, FullAccountingConfig fullAccountingConfig) {
|
||||
this.accountingRule = accountingRule;
|
||||
this.fullAccountingConfig = fullAccountingConfig;
|
||||
@@ -640,6 +645,11 @@ public class LoanProductTestBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoanProductTestBuilder withRecalculationRestFrequencyType(final String recalculationRestFrequencyType) {
|
||||
this.recalculationRestFrequencyType = recalculationRestFrequencyType;
|
||||
return this;
|
||||
}
|
||||
|
||||
public LoanProductTestBuilder withInterestRecalculationCompoundingFrequencyDetails(final String recalculationCompoundingFrequencyType,
|
||||
final String recalculationCompoundingFrequencyInterval, final Integer recalculationCompoundingFrequencyOnDayType,
|
||||
final Integer recalculationCompoundingFrequencyDayOfWeekType) {
|
||||
|
||||
Reference in New Issue
Block a user