FINERACT-2421: Allow multiple interest rate schedule adjustments same date

This commit is contained in:
Jose Alberto Hernandez
2026-01-27 15:04:30 +01:00
committed by Adam Saghy
parent f40ce6d9f6
commit 532190a649
5 changed files with 33 additions and 42 deletions
@@ -33,7 +33,6 @@ import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.Loan
import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateSupportedParameters; import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateSupportedParameters;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import jakarta.persistence.criteria.Predicate;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.ArrayList; import java.util.ArrayList;
@@ -44,17 +43,14 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRescheduleRequestToTermVariationMapping; import org.apache.fineract.portfolio.loanaccount.domain.LoanRescheduleRequestToTermVariationMapping;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansApiConstants; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansApiConstants;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component("progressiveLoanRescheduleRequestDataValidatorImpl") @Component("progressiveLoanRescheduleRequestDataValidatorImpl")
@@ -62,7 +58,6 @@ import org.springframework.stereotype.Component;
public class ProgressiveLoanRescheduleRequestDataValidator implements LoanRescheduleRequestDataValidator { public class ProgressiveLoanRescheduleRequestDataValidator implements LoanRescheduleRequestDataValidator {
private final FromJsonHelper fromJsonHelper; private final FromJsonHelper fromJsonHelper;
private final LoanRescheduleRequestRepository loanRescheduleRequestRepository;
@Override @Override
public void validateForCreateAction(JsonCommand jsonCommand, Loan loan) { public void validateForCreateAction(JsonCommand jsonCommand, Loan loan) {
@@ -115,9 +110,6 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
private void validateInterestRate(DataValidatorBuilder dataValidatorBuilder, Loan loan, LocalDate rescheduleFromDate) { private void validateInterestRate(DataValidatorBuilder dataValidatorBuilder, Loan loan, LocalDate rescheduleFromDate) {
validateLoanStatusIsActiveOrClosed(loan, dataValidatorBuilder); validateLoanStatusIsActiveOrClosed(loan, dataValidatorBuilder);
if (rescheduleFromDate != null) {
validateInterestRateChangeRescheduleFromDate(loan, rescheduleFromDate);
}
LoanRepaymentScheduleInstallment installment; LoanRepaymentScheduleInstallment installment;
installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate); installment = loan.getRelatedRepaymentScheduleInstallment(rescheduleFromDate);
validateReschedulingInstallment(dataValidatorBuilder, installment); validateReschedulingInstallment(dataValidatorBuilder, installment);
@@ -223,20 +215,6 @@ public class ProgressiveLoanRescheduleRequestDataValidator implements LoanResche
throw new UnsupportedOperationException("Nothing to override here"); throw new UnsupportedOperationException("Nothing to override here");
} }
private void validateInterestRateChangeRescheduleFromDate(Loan loan, LocalDate rescheduleFromDate) {
boolean alreadyExistInterestRateChange = loanRescheduleRequestRepository.exists((root, query, criteriaBuilder) -> {
Predicate loanPredicate = criteriaBuilder.equal(root.get("loan"), loan);
Predicate statusPredicate = root.get("statusEnum")
.in(List.of(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), LoanStatus.APPROVED.getValue()));
Predicate datePredicate = criteriaBuilder.equal(root.get("rescheduleFromDate"), rescheduleFromDate);
return criteriaBuilder.and(loanPredicate, statusPredicate, datePredicate);
});
if (alreadyExistInterestRateChange) {
throw new GeneralPlatformDomainRuleException("loan.reschedule.interest.rate.change.already.exists",
"Interest rate change for the provided date is already exists.", rescheduleFromDate);
}
}
private BigDecimal validateInterestRateParam(final FromJsonHelper fromJsonHelper, final JsonElement jsonElement, private BigDecimal validateInterestRateParam(final FromJsonHelper fromJsonHelper, final JsonElement jsonElement,
DataValidatorBuilder dataValidatorBuilder, Loan loan) { DataValidatorBuilder dataValidatorBuilder, Loan loan) {
final BigDecimal interestRate = fromJsonHelper final BigDecimal interestRate = fromJsonHelper
@@ -28,7 +28,6 @@ import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansApiConstants; 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.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -50,15 +49,11 @@ class ProgressiveLoanRescheduleRequestDataValidatorTest {
@Mock @Mock
private FromJsonHelper fromJsonHelper; private FromJsonHelper fromJsonHelper;
@Mock
private LoanRescheduleRequestRepository loanRescheduleRequestRepository;
private ProgressiveLoanRescheduleRequestDataValidator progressiveLoanRescheduleRequestDataValidator; private ProgressiveLoanRescheduleRequestDataValidator progressiveLoanRescheduleRequestDataValidator;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
progressiveLoanRescheduleRequestDataValidator = new ProgressiveLoanRescheduleRequestDataValidator(fromJsonHelper, progressiveLoanRescheduleRequestDataValidator = new ProgressiveLoanRescheduleRequestDataValidator(fromJsonHelper);
loanRescheduleRequestRepository);
when(dataValidatorBuilder.reset()).thenReturn(dataValidatorBuilder); when(dataValidatorBuilder.reset()).thenReturn(dataValidatorBuilder);
when(dataValidatorBuilder.parameter(anyString())).thenReturn(dataValidatorBuilder); when(dataValidatorBuilder.parameter(anyString())).thenReturn(dataValidatorBuilder);
} }
@@ -74,6 +74,7 @@ import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansA
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidator; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidator;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanTermVariationsRepository;
import org.apache.fineract.portfolio.loanaccount.rescheduleloan.exception.LoanRescheduleRequestNotFoundException; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.exception.LoanRescheduleRequestNotFoundException;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
@@ -117,6 +118,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
private final LoanScheduleComponent loanSchedule; private final LoanScheduleComponent loanSchedule;
private final LoanTransactionRepository loanTransactionRepository; private final LoanTransactionRepository loanTransactionRepository;
private final LoanLifecycleStateMachine loanLifecycleStateMachine; private final LoanLifecycleStateMachine loanLifecycleStateMachine;
private final LoanTermVariationsRepository loanTermVariationsRepository;
/** /**
* create a new instance of the LoanRescheduleRequest object from the JsonCommand object and persist * create a new instance of the LoanRescheduleRequest object from the JsonCommand object and persist
@@ -399,6 +401,17 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
hasInterestRateChange = true; hasInterestRateChange = true;
} }
} }
if (hasInterestRateChange && loan.isProgressiveSchedule()) {
final LocalDate termApplicableFromDate = rescheduleFromDate;
LoanTermVariations previousLoanTermVariations = activeLoanTermVariations.stream()
.filter(lt -> lt.getTermType().isInterestRateFromInstallment() //
&& lt.getTermApplicableFrom().equals(termApplicableFromDate))
.findFirst().orElse(null);
if (previousLoanTermVariations != null) {
previousLoanTermVariations.markAsInactive();
loanTermVariationsRepository.save(previousLoanTermVariations);
}
}
BigDecimal annualNominalInterestRate = null; BigDecimal annualNominalInterestRate = null;
List<LoanTermVariationsData> loanTermVariations = new ArrayList<>(); List<LoanTermVariationsData> loanTermVariations = new ArrayList<>();
loanTermVariationsMapper.constructLoanTermVariations(scheduleGeneratorDTO.getFloatingRateDTO(), annualNominalInterestRate, loanTermVariationsMapper.constructLoanTermVariations(scheduleGeneratorDTO.getFloatingRateDTO(), annualNominalInterestRate,
@@ -290,13 +290,9 @@ public class LoanRescheduleRequestTest extends BaseLoanIntegrationTest {
.loanId(loanResponse.get().getLoanId()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("15 February 2023") .loanId(loanResponse.get().getLoanId()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("15 February 2023")
.newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("16 February 2023"))); .newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("16 February 2023")));
exception = assertThrows(CallFailedRuntimeException.class, loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()
() -> loanRescheduleRequestHelper .loanId(loanResponse.get().getLoanId()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("15 February 2023")
.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanResponse.get().getLoanId()) .newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("16 February 2023"));
.dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("15 February 2023")
.newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("16 February 2023")));
assertEquals(403, exception.getResponse().code());
assertTrue(exception.getMessage().contains("loan.reschedule.interest.rate.change.already.exists"));
}); });
// Do not allow approve an interest rate change if the reschedule from date is not in the future // Do not allow approve an interest rate change if the reschedule from date is not in the future
// Do not allow create interest rate change if a previous interest rate change got already approved for that // Do not allow create interest rate change if a previous interest rate change got already approved for that
@@ -310,14 +306,17 @@ public class LoanRescheduleRequestTest extends BaseLoanIntegrationTest {
loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(), loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(),
new PostUpdateRescheduleLoansRequest().approvedOnDate("17 February 2024").locale("en").dateFormat(DATETIME_PATTERN)); new PostUpdateRescheduleLoansRequest().approvedOnDate("17 February 2024").locale("en").dateFormat(DATETIME_PATTERN));
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, PostCreateRescheduleLoansResponse secondRescheduleLoansResponse = loanRescheduleRequestHelper
() -> loanRescheduleRequestHelper .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanResponse.get().getLoanId())
.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(rescheduleLoansResponse.getLoanId()) .dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("17 February 2023").newInterestRate(BigDecimal.TEN)
.dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("17 February 2023") .rescheduleReasonId(1L).rescheduleFromDate("17 February 2023"));
.newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("17 February 2023"))); GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.get().getLoanId());
assertEquals(403, exception.getResponse().code()); assertEquals(loanDetails.getSummary().getInterestCharged().stripTrailingZeros(), BigDecimal.valueOf(1.53).stripTrailingZeros());
assertTrue(exception.getMessage().contains("loan.reschedule.interest.rate.change.already.exists"));
loanRescheduleRequestHelper.approveLoanRescheduleRequest(secondRescheduleLoansResponse.getResourceId(),
new PostUpdateRescheduleLoansRequest().approvedOnDate("17 February 2024").locale("en").dateFormat(DATETIME_PATTERN));
loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.get().getLoanId());
assertEquals(loanDetails.getSummary().getInterestCharged().stripTrailingZeros(), BigDecimal.valueOf(4.22).stripTrailingZeros());
}); });
// Allow new interest rate change if the previous got rejected // Allow new interest rate change if the previous got rejected
@@ -23,6 +23,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import io.restassured.specification.RequestSpecification; import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification; import io.restassured.specification.ResponseSpecification;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import org.apache.fineract.client.models.GetLoanRescheduleRequestResponse; import org.apache.fineract.client.models.GetLoanRescheduleRequestResponse;
import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest; import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest;
import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse; import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse;
@@ -118,4 +119,9 @@ public class LoanRescheduleRequestHelper {
return Calls return Calls
.ok(FineractClientHelper.getFineractClient().rescheduleLoans.updateLoanRescheduleRequest(scheduleId, request, "reject")); .ok(FineractClientHelper.getFineractClient().rescheduleLoans.updateLoanRescheduleRequest(scheduleId, request, "reject"));
} }
public List<GetLoanRescheduleRequestResponse> retrieveLoanRescheduleRequestsByLoan(final String command, final Long loanId) {
return Calls.ok(FineractClientHelper.getFineractClient().rescheduleLoans.retrieveAllRescheduleRequest(command, loanId));
}
} }