From fc4d41d48307880fc817e40e9a57ab1a8949affd Mon Sep 17 00:00:00 2001 From: Jose Alberto Hernandez Date: Tue, 20 Jan 2026 21:43:23 -0500 Subject: [PATCH] FINERACT-2421: Allow multiple interest rate schedule adjustments same date --- .../data/LoanRescheduleRequestData.java | 81 +----------------- .../LoanRescheduleRequestTimelineData.java | 27 +----- .../LoanRescheduleRequestRepository.java | 11 +++ .../mapper/LoanRescheduleRequestMapper.java | 84 +++++++++++++++++++ ...cheduleRequestReadPlatformServiceImpl.java | 40 ++++----- ...iveLoanRescheduleRequestDataValidator.java | 22 ++--- .../LoanRescheduleRequestTest.java | 75 ++++++++++++----- .../common/LoanRescheduleRequestHelper.java | 5 ++ 8 files changed, 186 insertions(+), 159 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/mapper/LoanRescheduleRequestMapper.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestData.java index 017abc198ac..d5dc0721151 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestData.java @@ -20,12 +20,16 @@ import java.time.LocalDate; import java.util.Collection; +import lombok.Data; +import lombok.RequiredArgsConstructor; import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; /** * Immutable data object representing loan reschedule request data. **/ +@Data +@RequiredArgsConstructor public final class LoanRescheduleRequestData { private final Long id; @@ -126,83 +130,6 @@ public static LoanRescheduleRequestData instance(Long id, Long loanId, LoanResch rescheduleReasonCodeValue); } - /** - * @return the id - */ - public Long getId() { - return id; - } - - /** - * @return the loanId - */ - public Long getLoanId() { - return loanId; - } - - /** - * @return the statusEnum - */ - public LoanRescheduleRequestStatusEnumData getStatusEnum() { - return statusEnum; - } - - /** - * @return the reschedule from installment number - */ - public Integer getRescheduleFromInstallment() { - return rescheduleFromInstallment; - } - - /** - * @return the reschedule from date - */ - public LocalDate getRescheduleFromDate() { - return rescheduleFromDate; - } - - /** - * @return the rescheduleReasonCodeValueId - */ - public CodeValueData getRescheduleReasonCodeValueId() { - return rescheduleReasonCodeValue; - } - - /** - * @return the rescheduleReasonText - */ - public String getRescheduleReasonComment() { - return rescheduleReasonComment; - } - - /** - * @return the timeline - **/ - public LoanRescheduleRequestTimelineData getTimeline() { - return this.timeline; - } - - /** - * @return the clientName - */ - public String getClientName() { - return clientName; - } - - /** - * @return the loanAccountNumber - */ - public String getLoanAccountNumber() { - return loanAccountNumber; - } - - /** - * @return the clientId - */ - public Long getClientId() { - return clientId; - } - /** * @return the recalculateInterest */ diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestTimelineData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestTimelineData.java index 8715f016435..7ca09655dc5 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestTimelineData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/LoanRescheduleRequestTimelineData.java @@ -19,11 +19,15 @@ package org.apache.fineract.portfolio.loanaccount.rescheduleloan.data; import java.time.LocalDate; +import lombok.AllArgsConstructor; +import lombok.Data; /** * Immutable data object represent the timeline events of a loan reschedule request **/ @SuppressWarnings("unused") +@Data +@AllArgsConstructor public class LoanRescheduleRequestTimelineData { private final LocalDate submittedOnDate; @@ -40,27 +44,4 @@ public class LoanRescheduleRequestTimelineData { private final String rejectedByUsername; private final String rejectedByFirstname; private final String rejectedByLastname; - - public LoanRescheduleRequestTimelineData(final LocalDate submittedOnDate, final String submittedByUsername, - final String submittedByFirstname, final String submittedByLastname, final LocalDate approvedOnDate, - final String approvedByUsername, final String approvedByFirstname, final String approvedByLastname, - final LocalDate rejectedOnDate, final String rejectedByUsername, final String rejectedByFirstname, - final String rejectedByLastname) { - - this.submittedOnDate = submittedOnDate; - this.submittedByUsername = submittedByUsername; - this.submittedByFirstname = submittedByFirstname; - this.submittedByLastname = submittedByLastname; - - this.approvedOnDate = approvedOnDate; - this.approvedByUsername = approvedByUsername; - this.approvedByFirstname = approvedByFirstname; - this.approvedByLastname = approvedByLastname; - - this.rejectedOnDate = rejectedOnDate; - this.rejectedByUsername = rejectedByUsername; - this.rejectedByFirstname = rejectedByFirstname; - this.rejectedByLastname = rejectedByLastname; - - } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequestRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequestRepository.java index 42e4b194c29..6f357def408 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequestRepository.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/domain/LoanRescheduleRequestRepository.java @@ -18,7 +18,10 @@ */ package org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain; +import java.time.LocalDate; +import java.util.List; import java.util.Optional; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Query; @@ -29,4 +32,12 @@ public interface LoanRescheduleRequestRepository @Query("select lrr.loan.id from LoanRescheduleRequest lrr where lrr.id = :rescheduleRequestId") Optional getLoanIdByRescheduleRequestId(@Param("rescheduleRequestId") Long rescheduleRequestId); + + @Query("select lrr from LoanRescheduleRequest lrr where lrr.loan = :loan and lrr.rescheduleFromDate = :rescheduleFromDate and lrr.statusEnum in :statuses") + Optional fetchByLoanAndFromDateAndStatus(@Param("loan") Loan loan, + @Param("rescheduleFromDate") LocalDate rescheduleFromDate, @Param("statuses") List statuses); + + @Query("select lrr from LoanRescheduleRequest lrr where lrr.loan.id = :loanId") + List fetchByLoanId(@Param("loanId") Long loanId); + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/mapper/LoanRescheduleRequestMapper.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/mapper/LoanRescheduleRequestMapper.java new file mode 100644 index 00000000000..ae0f51fa1a0 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/mapper/LoanRescheduleRequestMapper.java @@ -0,0 +1,84 @@ +/** + * 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.mapper; + +import java.util.List; +import java.util.Set; +import org.apache.fineract.infrastructure.codes.mapper.CodeValueMapper; +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRescheduleRequestToTermVariationMapping; +import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestData; +import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestEnumerations; +import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestStatusEnumData; +import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestTimelineData; +import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; + +@Mapper(config = MapstructMapperConfig.class, uses = { CodeValueMapper.class }) +public interface LoanRescheduleRequestMapper { + + @Mapping(target = "id", source = "source.id") + @Mapping(target = "loanId", source = "source.loan.id") + @Mapping(target = "loanAccountNumber", source = "source.loan.accountNumber") + @Mapping(target = "clientId", source = "source.loan.client.id") + @Mapping(target = "clientName", source = "source.loan.client.displayName") + @Mapping(target = "statusEnum", source = "source.statusEnum", qualifiedByName = "loanRescheduleRequestStatus") + @Mapping(target = "rescheduleReasonCodeValue", source = "source.rescheduleReasonCodeValue") + @Mapping(target = "loanTermVariationsData", source = "source.loanRescheduleRequestToTermVariationMappings", qualifiedByName = "loanTermVariationsData") + @Mapping(target = "timeline", source = "source", qualifiedByName = "loanRescheduleRequestStatus") + @Mapping(target = "rescheduleReasons", ignore = true) + LoanRescheduleRequestData map(LoanRescheduleRequest source); + + List map(List sources); + + @Named("loanRescheduleRequestStatus") + default LoanRescheduleRequestStatusEnumData loanRescheduleRequestStatus(Integer statusEnumId) { + return LoanRescheduleRequestEnumerations.status(statusEnumId); + } + + @Named("loanTermVariationsData") + default List loanTermVariationsData( + Set loanRescheduleRequestToTermVariationMappings) { + return loanRescheduleRequestToTermVariationMappings.stream().map(m -> m.getLoanTermVariations().toData()).toList(); + } + + @Named("loanRescheduleRequestStatus") + default LoanRescheduleRequestTimelineData loanRescheduleRequestTimeline(LoanRescheduleRequest request) { + final LoanRescheduleRequestStatusEnumData loanRescheduleRequestStatus = loanRescheduleRequestStatus(request.getStatusEnum()); + if (loanRescheduleRequestStatus.isPendingApproval()) { + return new LoanRescheduleRequestTimelineData(request.getSubmittedOnDate(), request.getSubmittedByUser().getUsername(), + request.getSubmittedByUser().getFirstname(), request.getSubmittedByUser().getLastname(), null, null, null, null, null, + null, null, null); + } else if (loanRescheduleRequestStatus.isApproved()) { + return new LoanRescheduleRequestTimelineData(request.getSubmittedOnDate(), request.getSubmittedByUser().getUsername(), + request.getSubmittedByUser().getFirstname(), request.getSubmittedByUser().getLastname(), request.getApprovedOnDate(), + request.getApprovedByUser().getUsername(), request.getApprovedByUser().getFirstname(), + request.getApprovedByUser().getLastname(), null, null, null, null); + } else { + return new LoanRescheduleRequestTimelineData(request.getSubmittedOnDate(), request.getSubmittedByUser().getUsername(), + request.getSubmittedByUser().getFirstname(), request.getSubmittedByUser().getLastname(), null, null, null, null, + request.getRejectedOnDate(), request.getRejectedByUser().getUsername(), request.getRejectedByUser().getFirstname(), + request.getRejectedByUser().getLastname()); + } + } + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java index ce6c3e8b30d..1a2ebfbf223 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestReadPlatformServiceImpl.java @@ -27,39 +27,37 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import lombok.RequiredArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.codes.data.CodeValueData; import org.apache.fineract.infrastructure.codes.service.CodeValueReadPlatformService; import org.apache.fineract.infrastructure.core.domain.JdbcSupport; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.RescheduleLoansApiConstants; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestData; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestEnumerations; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestStatusEnumData; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestTimelineData; +import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository; +import org.apache.fineract.portfolio.loanaccount.rescheduleloan.exception.LoanRescheduleRequestNotFoundException; +import org.apache.fineract.portfolio.loanaccount.rescheduleloan.mapper.LoanRescheduleRequestMapper; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; @Service +@RequiredArgsConstructor public class LoanRescheduleRequestReadPlatformServiceImpl implements LoanRescheduleRequestReadPlatformService { private final JdbcTemplate jdbcTemplate; private final LoanRepositoryWrapper loanRepositoryWrapper; private static final LoanRescheduleRequestRowMapper LOAN_RESCHEDULE_REQUEST_ROW_MAPPER = new LoanRescheduleRequestRowMapper(); private final CodeValueReadPlatformService codeValueReadPlatformService; - - @Autowired - public LoanRescheduleRequestReadPlatformServiceImpl(final JdbcTemplate jdbcTemplate, LoanRepositoryWrapper loanRepositoryWrapper, - final CodeValueReadPlatformService codeValueReadPlatformService) { - this.jdbcTemplate = jdbcTemplate; - this.loanRepositoryWrapper = loanRepositoryWrapper; - this.codeValueReadPlatformService = codeValueReadPlatformService; - } + private final LoanRescheduleRequestRepository loanRescheduleRequestRepository; + private final LoanRescheduleRequestMapper loanRescheduleRequestMapper; private static final class LoanRescheduleRequestRowMapper implements RowMapper { @@ -222,24 +220,17 @@ public LoanRescheduleRequestData mapRow(final ResultSet rs, @SuppressWarnings("u @Override public List readLoanRescheduleRequests(Long loanId) { - this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); - final String sql = "select " + LOAN_RESCHEDULE_REQUEST_ROW_MAPPER.schema() + " where lr.loan_id = ?"; + if (!loanRepositoryWrapper.existsByLoanId(loanId)) { + throw new LoanNotFoundException(loanId); + } - return this.jdbcTemplate.query(sql, LOAN_RESCHEDULE_REQUEST_ROW_MAPPER, loanId); // NOSONAR + return loanRescheduleRequestMapper.map(loanRescheduleRequestRepository.fetchByLoanId(loanId)); } @Override public LoanRescheduleRequestData readLoanRescheduleRequest(Long requestId) { - - try { - final String sql = "select " + LOAN_RESCHEDULE_REQUEST_ROW_MAPPER.schema() + " where lr.id = ?"; - - return this.jdbcTemplate.queryForObject(sql, LOAN_RESCHEDULE_REQUEST_ROW_MAPPER, requestId); // NOSONAR - } - - catch (final EmptyResultDataAccessException e) { - return null; - } + return loanRescheduleRequestMapper.map(loanRescheduleRequestRepository.findById(requestId) // + .orElseThrow(() -> new LoanRescheduleRequestNotFoundException(requestId))); } @Override @@ -292,8 +283,7 @@ public List retrieveAllRescheduleRequests(String comm } if (loanId != null) { - extraFilters.add("loan.id = ?"); - extraParams.add(loanId); + return readLoanRescheduleRequests(loanId); } if (isNotEmpty(extraFilters)) { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java index 20f14807e17..763f851382e 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/data/ProgressiveLoanRescheduleRequestDataValidator.java @@ -33,18 +33,17 @@ import static org.apache.fineract.portfolio.loanaccount.rescheduleloan.data.LoanRescheduleRequestDataValidatorImpl.validateSupportedParameters; import com.google.gson.JsonElement; -import jakarta.persistence.criteria.Predicate; import java.math.BigDecimal; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.stream.Stream; import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.data.ApiParameterError; 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.serialization.FromJsonHelper; import org.apache.fineract.portfolio.loanaccount.domain.Loan; @@ -223,17 +222,14 @@ public void validateForRejectAction(JsonCommand jsonCommand, LoanRescheduleReque 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 void validateInterestRateChangeRescheduleFromDate(final Loan loan, final LocalDate rescheduleFromDate) { + Optional optLoanRescheduleRequest = loanRescheduleRequestRepository.fetchByLoanAndFromDateAndStatus(loan, // + rescheduleFromDate, List.of(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL.getValue(), LoanStatus.APPROVED.getValue())); + + if (optLoanRescheduleRequest.isPresent()) { + LoanRescheduleRequest loanRescheduleRequest = optLoanRescheduleRequest.get(); + loanRescheduleRequest.reject(loanRescheduleRequest.getSubmittedByUser(), rescheduleFromDate); + loanRescheduleRequestRepository.save(loanRescheduleRequest); } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java index 93da039e9ca..d2a8df3c3e4 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRescheduleRequestTest.java @@ -20,6 +20,7 @@ import static org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder.DEFAULT_STRATEGY; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -45,6 +46,7 @@ import org.apache.fineract.client.models.PostLoansRequest; import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest; +import org.apache.fineract.client.models.PostUpdateRescheduleLoansResponse; import org.apache.fineract.client.util.CallFailedRuntimeException; import org.apache.fineract.integrationtests.common.ClientHelper; import org.apache.fineract.integrationtests.common.CollateralManagementHelper; @@ -264,7 +266,7 @@ public void testInterestRateChangeForProgressiveLoan() { AtomicReference rescheduleResponse = new AtomicReference<>(); AtomicReference loanResponse = new AtomicReference<>(); // Do not allow interest rate change on not active loan - // Do not allow interest rate change twice on the same day + // Allow interest rate change twice on the same day runAt("15 February 2023", () -> { loanResponse.set(applyForLoanApplication(client.getClientId(), commonLoanProductId, BigDecimal.valueOf(500.0), 45, 15, 3, @@ -286,38 +288,69 @@ public void testInterestRateChangeForProgressiveLoan() { new PostLoansLoanIdRequest().actualDisbursementDate("15 February 2023").dateFormat(DATETIME_PATTERN) .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); - rescheduleResponse.set(loanRescheduleRequestHelper.createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest() - .loanId(loanResponse.get().getLoanId()).dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("15 February 2023") - .newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("16 February 2023"))); + PostCreateRescheduleLoansResponse firstLoanRescheduleRequest = loanRescheduleRequestHelper + .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanResponse.get().getLoanId()) + .dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("15 February 2023").newInterestRate(BigDecimal.ONE) + .rescheduleReasonId(1L).rescheduleFromDate("16 February 2023")); + rescheduleResponse.set(firstLoanRescheduleRequest); - exception = assertThrows(CallFailedRuntimeException.class, - () -> loanRescheduleRequestHelper - .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanResponse.get().getLoanId()) - .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")); + PostCreateRescheduleLoansResponse secondLoanRescheduleRequest = loanRescheduleRequestHelper + .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanResponse.get().getLoanId()) + .dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("15 February 2023").newInterestRate(BigDecimal.TWO) + .rescheduleReasonId(1L).rescheduleFromDate("16 February 2023")); + + final List loanRescheduleRequests = loanRescheduleRequestHelper + .retrieveLoanRescheduleRequestsByLoan(null, loanResponse.get().getLoanId()); + assertNotNull(loanRescheduleRequests); + assertEquals(loanRescheduleRequests.size(), 2); + final GetLoanRescheduleRequestResponse firstLoanRescheduleRequestData = loanRescheduleRequests.get(0); + final GetLoanRescheduleRequestResponse secondLoanRescheduleRequestData = loanRescheduleRequests.get(1); + + assertEquals(firstLoanRescheduleRequestData.getId(), firstLoanRescheduleRequest.getResourceId()); + assertTrue(firstLoanRescheduleRequestData.getStatusEnum().getRejected()); + assertEquals(secondLoanRescheduleRequestData.getId(), secondLoanRescheduleRequest.getResourceId()); + assertTrue(secondLoanRescheduleRequestData.getStatusEnum().getPendingApproval()); + assertEquals(firstLoanRescheduleRequestData.getRescheduleFromDate(), secondLoanRescheduleRequestData.getRescheduleFromDate()); + assertEquals( + firstLoanRescheduleRequestData.getLoanTermVariationsData().iterator().next().getDecimalValue().stripTrailingZeros(), + BigDecimal.ONE.stripTrailingZeros()); + assertEquals( + secondLoanRescheduleRequestData.getLoanTermVariationsData().iterator().next().getDecimalValue().stripTrailingZeros(), + BigDecimal.TWO.stripTrailingZeros()); }); // 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 - // date + // Allow create interest rate change if a previous interest rate change got already approved for that date runAt("16 February 2023", () -> { PostCreateRescheduleLoansResponse rescheduleLoansResponse = loanRescheduleRequestHelper .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(loanResponse.get().getLoanId()) .dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("17 February 2023").newInterestRate(BigDecimal.ONE) .rescheduleReasonId(1L).rescheduleFromDate("17 February 2023")); - loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(), + PostUpdateRescheduleLoansResponse approvedLoanRescheduleRequest = loanRescheduleRequestHelper.approveLoanRescheduleRequest( + rescheduleLoansResponse.getResourceId(), new PostUpdateRescheduleLoansRequest().approvedOnDate("17 February 2024").locale("en").dateFormat(DATETIME_PATTERN)); - CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class, - () -> loanRescheduleRequestHelper - .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(rescheduleLoansResponse.getLoanId()) - .dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("17 February 2023") - .newInterestRate(BigDecimal.ONE).rescheduleReasonId(1L).rescheduleFromDate("17 February 2023"))); - assertEquals(403, exception.getResponse().code()); - assertTrue(exception.getMessage().contains("loan.reschedule.interest.rate.change.already.exists")); + loanRescheduleRequestHelper + .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest().loanId(rescheduleLoansResponse.getLoanId()) + .dateFormat(DATETIME_PATTERN).locale("en").submittedOnDate("17 February 2023").newInterestRate(BigDecimal.TEN) + .rescheduleReasonId(1L).rescheduleFromDate("17 February 2023")); + final List loanRescheduleRequests = loanRescheduleRequestHelper + .retrieveLoanRescheduleRequestsByLoan(null, loanResponse.get().getLoanId()); + assertNotNull(loanRescheduleRequests); + assertEquals(loanRescheduleRequests.size(), 4); + final GetLoanRescheduleRequestResponse thirdLoanRescheduleRequestData = loanRescheduleRequests.get(2); + final GetLoanRescheduleRequestResponse fourthLoanRescheduleRequestData = loanRescheduleRequests.get(3); + + assertTrue(thirdLoanRescheduleRequestData.getStatusEnum().getRejected()); + assertTrue(fourthLoanRescheduleRequestData.getStatusEnum().getPendingApproval()); + assertEquals(thirdLoanRescheduleRequestData.getRescheduleFromDate(), fourthLoanRescheduleRequestData.getRescheduleFromDate()); + assertEquals( + thirdLoanRescheduleRequestData.getLoanTermVariationsData().iterator().next().getDecimalValue().stripTrailingZeros(), + BigDecimal.ONE.stripTrailingZeros()); + assertEquals( + fourthLoanRescheduleRequestData.getLoanTermVariationsData().iterator().next().getDecimalValue().stripTrailingZeros(), + BigDecimal.TEN.stripTrailingZeros()); }); // Allow new interest rate change if the previous got rejected diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java index e7a2d174033..2912d04ecfd 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/LoanRescheduleRequestHelper.java @@ -23,6 +23,7 @@ import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import java.util.HashMap; +import java.util.List; import org.apache.fineract.client.models.GetLoanRescheduleRequestResponse; import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest; import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse; @@ -118,4 +119,8 @@ public PostUpdateRescheduleLoansResponse rejectLoanRescheduleRequest(Long schedu return Calls .ok(FineractClientHelper.getFineractClient().rescheduleLoans.updateLoanRescheduleRequest(scheduleId, request, "reject")); } + + public List retrieveLoanRescheduleRequestsByLoan(final String command, final Long loanId) { + return Calls.ok(FineractClientHelper.getFineractClient().rescheduleLoans.retrieveAllRescheduleRequest(command, loanId)); + } }