FINERACT-1992 Delinquency Pause API Changes
This commit is contained in:
+9
@@ -3653,4 +3653,13 @@ public class CommandWrapperBuilder {
|
|||||||
this.href = "/loans/" + loanId + "/transactions?command=downPayment";
|
this.href = "/loans/" + loanId + "/transactions?command=downPayment";
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CommandWrapperBuilder createDelinquencyAction(final Long loanId) {
|
||||||
|
this.actionName = "CREATE";
|
||||||
|
this.entityName = "DELINQUENCY_ACTION";
|
||||||
|
this.entityId = null;
|
||||||
|
this.loanId = loanId;
|
||||||
|
this.href = "/loans/" + loanId + "/delinquency-action";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+52
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.delinquency.api;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
|
||||||
public final class DelinquencyApiResourceSwagger {
|
public final class DelinquencyApiResourceSwagger {
|
||||||
|
|
||||||
@@ -150,4 +151,55 @@ public final class DelinquencyApiResourceSwagger {
|
|||||||
public LocalDate liftedOnDate;
|
public LocalDate liftedOnDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Schema(description = "GetDelinquencyActionsResponse")
|
||||||
|
public static final class GetDelinquencyActionsResponse {
|
||||||
|
|
||||||
|
private GetDelinquencyActionsResponse() {}
|
||||||
|
|
||||||
|
@Schema(example = "1")
|
||||||
|
public Long id;
|
||||||
|
@Schema(example = "pause")
|
||||||
|
public String action;
|
||||||
|
@Schema(example = "2013,1,2")
|
||||||
|
public LocalDate startDate;
|
||||||
|
@Schema(example = "2013,2,20")
|
||||||
|
public LocalDate endDate;
|
||||||
|
@Schema(example = "1")
|
||||||
|
public Long createdById;
|
||||||
|
@Schema(example = "1359463135000")
|
||||||
|
public OffsetDateTime createdOn;
|
||||||
|
@Schema(example = "1")
|
||||||
|
public Long updatedById;
|
||||||
|
@Schema(example = "1359463135000")
|
||||||
|
public OffsetDateTime lastModifiedOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Schema(description = "PostLoansDelinquencyActionRequest")
|
||||||
|
public static final class PostLoansDelinquencyActionRequest {
|
||||||
|
|
||||||
|
@Schema(example = "pause")
|
||||||
|
public String action;
|
||||||
|
@Schema(example = "2013-01-02")
|
||||||
|
public String startDate;
|
||||||
|
@Schema(example = "2013-02-20")
|
||||||
|
public String endDate;
|
||||||
|
@Schema(example = "yyyy-MM-dd")
|
||||||
|
public String dateFormat;
|
||||||
|
@Schema(example = "en")
|
||||||
|
public String locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Schema(description = "PostLoansDelinquencyActionResponse")
|
||||||
|
public static final class PostLoansDelinquencyActionResponse {
|
||||||
|
|
||||||
|
@Schema(example = "1")
|
||||||
|
public Long officeId;
|
||||||
|
|
||||||
|
@Schema(example = "1")
|
||||||
|
public Long clientId;
|
||||||
|
|
||||||
|
@Schema(example = "1")
|
||||||
|
public Long resourceId;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -19,6 +19,8 @@
|
|||||||
package org.apache.fineract.portfolio.delinquency.domain;
|
package org.apache.fineract.portfolio.delinquency.domain;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
@@ -35,4 +37,7 @@ public interface LoanDelinquencyActionRepository
|
|||||||
+ " da.loan.id = :loan_id order by da.createdDate desc")
|
+ " da.loan.id = :loan_id order by da.createdDate desc")
|
||||||
Page<LoanDelinquencyAction> getEffectiveDelinquencyActionForLoan(@Param("loan_id") Long loan_id,
|
Page<LoanDelinquencyAction> getEffectiveDelinquencyActionForLoan(@Param("loan_id") Long loan_id,
|
||||||
@Param("business_date") LocalDate business_date, Pageable page);
|
@Param("business_date") LocalDate business_date, Pageable page);
|
||||||
|
|
||||||
|
List<LoanDelinquencyAction> findByLoanOrderById(Loan loan);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 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.delinquency.handler;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.commands.annotation.CommandType;
|
||||||
|
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@CommandType(entity = "DELINQUENCY_ACTION", action = "CREATE")
|
||||||
|
public class CreateDelinquencyActionCommandHandler implements NewCommandSourceHandler {
|
||||||
|
|
||||||
|
private final DelinquencyWritePlatformService writePlatformService;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult processCommand(final JsonCommand command) {
|
||||||
|
return this.writePlatformService.createDelinquencyAction(command.getLoanId(), command);
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
@@ -23,6 +23,7 @@ import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
|
|||||||
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
|
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
|
||||||
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
|
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
|
||||||
import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
|
import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
||||||
|
|
||||||
public interface DelinquencyReadPlatformService {
|
public interface DelinquencyReadPlatformService {
|
||||||
@@ -43,4 +44,6 @@ public interface DelinquencyReadPlatformService {
|
|||||||
|
|
||||||
Collection<LoanInstallmentDelinquencyTagData> retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId);
|
Collection<LoanInstallmentDelinquencyTagData> retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId);
|
||||||
|
|
||||||
|
Collection<LoanDelinquencyAction> retrieveLoanDelinquencyActions(Long loanId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+71
@@ -18,24 +18,37 @@
|
|||||||
*/
|
*/
|
||||||
package org.apache.fineract.portfolio.delinquency.service;
|
package org.apache.fineract.portfolio.delinquency.service;
|
||||||
|
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.RESUME;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
|
||||||
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
|
import org.apache.fineract.portfolio.delinquency.data.DelinquencyBucketData;
|
||||||
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
|
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
|
||||||
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
|
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
|
||||||
import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
|
import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucket;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
|
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
|
||||||
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
|
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
|
||||||
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
|
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||||
@@ -55,6 +68,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
|||||||
private final LoanRepository loanRepository;
|
private final LoanRepository loanRepository;
|
||||||
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
|
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
|
||||||
private final LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag;
|
private final LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag;
|
||||||
|
private final LoanDelinquencyActionRepository loanDelinquencyActionRepository;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
|
public Collection<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
|
||||||
@@ -124,14 +138,71 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
|||||||
collectionData.setLastRepaymentDate(lastRepaymentTransaction.getTransactionDate());
|
collectionData.setLastRepaymentDate(lastRepaymentTransaction.getTransactionDate());
|
||||||
collectionData.setLastRepaymentAmount(lastRepaymentTransaction.getAmount());
|
collectionData.setLastRepaymentAmount(lastRepaymentTransaction.getAmount());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enrichWithDelinquencyPausePeriodInfo(collectionData, retrieveLoanDelinquencyActions(loanId),
|
||||||
|
ThreadLocalContextUtil.getBusinessDate());
|
||||||
}
|
}
|
||||||
|
|
||||||
return collectionData;
|
return collectionData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void enrichWithDelinquencyPausePeriodInfo(CollectionData collectionData, Collection<LoanDelinquencyAction> delinquencyActions,
|
||||||
|
LocalDate businessDate) {
|
||||||
|
// partition them based on type
|
||||||
|
Map<DelinquencyAction, List<LoanDelinquencyAction>> partitioned = delinquencyActions.stream()
|
||||||
|
.collect(Collectors.groupingBy(LoanDelinquencyAction::getAction));
|
||||||
|
|
||||||
|
// add the possible resumes to it to create the effective pause periods
|
||||||
|
if (partitioned.containsKey(DelinquencyAction.PAUSE)) {
|
||||||
|
List<LoanDelinquencyActionData> effective = new ArrayList<>();
|
||||||
|
List<LoanDelinquencyAction> pauses = partitioned.get(DelinquencyAction.PAUSE);
|
||||||
|
for (LoanDelinquencyAction loanDelinquencyAction : pauses) {
|
||||||
|
Optional<LoanDelinquencyAction> resume = findMatchingResume(loanDelinquencyAction, partitioned.get(RESUME));
|
||||||
|
LoanDelinquencyActionData loanDelinquencyActionData = new LoanDelinquencyActionData(loanDelinquencyAction);
|
||||||
|
resume.ifPresent(r -> loanDelinquencyActionData.setEndDate(r.getStartDate()));
|
||||||
|
effective.add(loanDelinquencyActionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// order them by start date, filter out the future items
|
||||||
|
Optional<LoanDelinquencyActionData> last = effective.stream()
|
||||||
|
.sorted(Comparator.comparing(LoanDelinquencyActionData::getStartDate))
|
||||||
|
.filter(e -> !DateUtils.isAfter(e.getStartDate(), businessDate)) // keep only the present and the
|
||||||
|
// past ones
|
||||||
|
.reduce((first, second) -> second);
|
||||||
|
|
||||||
|
// enrich collectionData
|
||||||
|
last.ifPresent(action -> {
|
||||||
|
collectionData
|
||||||
|
.setDelinquencyCalculationPaused(!action.startDate.isAfter(businessDate) && !businessDate.isAfter(action.endDate));
|
||||||
|
collectionData.setDelinquencyPausePeriodStartDate(action.getStartDate());
|
||||||
|
collectionData.setDelinquencyPausePeriodEndDate(action.getEndDate());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<LoanDelinquencyAction> findMatchingResume(LoanDelinquencyAction pause, List<LoanDelinquencyAction> resumes) {
|
||||||
|
if (resumes != null && resumes.size() > 0) {
|
||||||
|
for (LoanDelinquencyAction resume : resumes) {
|
||||||
|
if (!pause.getStartDate().isAfter(resume.getStartDate()) && !resume.getStartDate().isAfter(pause.getEndDate())) {
|
||||||
|
return Optional.of(resume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Collection<LoanInstallmentDelinquencyTagData> retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId) {
|
public Collection<LoanInstallmentDelinquencyTagData> retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId) {
|
||||||
return repositoryLoanInstallmentDelinquencyTag.findInstallmentDelinquencyTags(loanId);
|
return repositoryLoanInstallmentDelinquencyTag.findInstallmentDelinquencyTags(loanId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Collection<LoanDelinquencyAction> retrieveLoanDelinquencyActions(Long loanId) {
|
||||||
|
final Optional<Loan> optLoan = this.loanRepository.findById(loanId);
|
||||||
|
if (optLoan.isPresent()) {
|
||||||
|
return loanDelinquencyActionRepository.findByLoanOrderById(optLoan.get());
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -47,4 +47,6 @@ public interface DelinquencyWritePlatformService {
|
|||||||
|
|
||||||
void applyDelinquencyTagToLoan(LoanScheduleDelinquencyData loanDelinquencyData);
|
void applyDelinquencyTagToLoan(LoanScheduleDelinquencyData loanDelinquencyData);
|
||||||
|
|
||||||
|
CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand command);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+27
@@ -44,12 +44,15 @@ import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMapping
|
|||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag;
|
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.exception.DelinquencyBucketAgesOverlapedException;
|
import org.apache.fineract.portfolio.delinquency.exception.DelinquencyBucketAgesOverlapedException;
|
||||||
import org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInvalidAgesException;
|
import org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInvalidAgesException;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
||||||
@@ -76,6 +79,8 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
|
|||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
|
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
|
||||||
private final LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository;
|
private final LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository;
|
||||||
|
private final LoanDelinquencyActionRepository loanDelinquencyActionRepository;
|
||||||
|
private final DelinquencyActionParseAndValidator delinquencyActionParseAndValidator;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommandProcessingResult createDelinquencyRange(JsonCommand command) {
|
public CommandProcessingResult createDelinquencyRange(JsonCommand command) {
|
||||||
@@ -199,6 +204,28 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand command) {
|
||||||
|
final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId);
|
||||||
|
final LocalDate businessDate = DateUtils.getBusinessLocalDate();
|
||||||
|
final List<LoanDelinquencyAction> savedDelinquencyList = loanDelinquencyActionRepository.findByLoanOrderById(loan);
|
||||||
|
|
||||||
|
LoanDelinquencyAction parsedDelinquencyAction = delinquencyActionParseAndValidator.validateAndParseUpdate(command, loan,
|
||||||
|
savedDelinquencyList, businessDate);
|
||||||
|
|
||||||
|
parsedDelinquencyAction.setLoan(loan);
|
||||||
|
|
||||||
|
LoanDelinquencyAction saved = loanDelinquencyActionRepository.saveAndFlush(parsedDelinquencyAction);
|
||||||
|
|
||||||
|
return new CommandProcessingResultBuilder().withCommandId(command.commandId()) //
|
||||||
|
.withEntityId(saved.getId()) //
|
||||||
|
.withOfficeId(loan.getOfficeId()) //
|
||||||
|
.withClientId(loan.getClientId()) //
|
||||||
|
.withGroupId(loan.getGroupId()) //
|
||||||
|
.withLoanId(loanId) //
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void removeDelinquencyTagToLoan(final Loan loan) {
|
public void removeDelinquencyTagToLoan(final Loan loan) {
|
||||||
setLoanDelinquencyTag(loan, null);
|
setLoanDelinquencyTag(loan, null);
|
||||||
|
|||||||
+10
-4
@@ -22,6 +22,7 @@ import org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
|
|||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappingsRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappingsRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
|
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
|
||||||
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
|
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
|
||||||
@@ -33,6 +34,7 @@ import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatfor
|
|||||||
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceImpl;
|
import org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceImpl;
|
||||||
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
|
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
|
||||||
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl;
|
import org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
|
import org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||||
@@ -52,10 +54,11 @@ public class DelinquencyConfiguration {
|
|||||||
DelinquencyRangeMapper mapperRange, DelinquencyBucketMapper mapperBucket,
|
DelinquencyRangeMapper mapperRange, DelinquencyBucketMapper mapperBucket,
|
||||||
LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory, LoanRepository loanRepository,
|
LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory, LoanRepository loanRepository,
|
||||||
LoanDelinquencyDomainService loanDelinquencyDomainService,
|
LoanDelinquencyDomainService loanDelinquencyDomainService,
|
||||||
LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag) {
|
LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag,
|
||||||
|
LoanDelinquencyActionRepository loanDelinquencyActionRepository) {
|
||||||
return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
|
return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
|
||||||
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService,
|
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService,
|
||||||
repositoryLoanInstallmentDelinquencyTag);
|
repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -66,10 +69,13 @@ public class DelinquencyConfiguration {
|
|||||||
LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository, LoanRepositoryWrapper loanRepository,
|
LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository, LoanRepositoryWrapper loanRepository,
|
||||||
LoanProductRepository loanProductRepository, BusinessEventNotifierService businessEventNotifierService,
|
LoanProductRepository loanProductRepository, BusinessEventNotifierService businessEventNotifierService,
|
||||||
LoanDelinquencyDomainService loanDelinquencyDomainService,
|
LoanDelinquencyDomainService loanDelinquencyDomainService,
|
||||||
LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository) {
|
LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository,
|
||||||
|
LoanDelinquencyActionRepository loanDelinquencyActionRepository,
|
||||||
|
DelinquencyActionParseAndValidator delinquencyActionParseAndValidator) {
|
||||||
return new DelinquencyWritePlatformServiceImpl(dataValidatorBucket, dataValidatorRange, repositoryRange, repositoryBucket,
|
return new DelinquencyWritePlatformServiceImpl(dataValidatorBucket, dataValidatorRange, repositoryRange, repositoryBucket,
|
||||||
repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, businessEventNotifierService,
|
repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, businessEventNotifierService,
|
||||||
loanDelinquencyDomainService, loanInstallmentDelinquencyTagRepository);
|
loanDelinquencyDomainService, loanInstallmentDelinquencyTagRepository, loanDelinquencyActionRepository,
|
||||||
|
delinquencyActionParseAndValidator);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
+31
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* 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.delinquency.validator;
|
||||||
|
|
||||||
|
public final class DelinquencyActionParameters {
|
||||||
|
|
||||||
|
private DelinquencyActionParameters() {}
|
||||||
|
|
||||||
|
public static final String ACTION = "action";
|
||||||
|
public static final String START_DATE = "startDate";
|
||||||
|
public static final String END_DATE = "endDate";
|
||||||
|
public static final String DATE_FORMAT = "dateFormat";
|
||||||
|
|
||||||
|
public static final String LOCALE = "locale";
|
||||||
|
}
|
||||||
+199
@@ -0,0 +1,199 @@
|
|||||||
|
/**
|
||||||
|
* 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.delinquency.validator;
|
||||||
|
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.RESUME;
|
||||||
|
|
||||||
|
import com.google.gson.JsonElement;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
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.exception.PlatformApiDataValidationException;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper;
|
||||||
|
import org.apache.fineract.infrastructure.core.service.DateUtils;
|
||||||
|
import org.apache.fineract.infrastructure.core.validator.ParseAndValidator;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Component
|
||||||
|
public class DelinquencyActionParseAndValidator extends ParseAndValidator {
|
||||||
|
|
||||||
|
private final FromJsonHelper jsonHelper;
|
||||||
|
|
||||||
|
public LoanDelinquencyAction validateAndParseUpdate(@NotNull final JsonCommand command, Loan loan,
|
||||||
|
List<LoanDelinquencyAction> savedDelinquencyActions, LocalDate businessDate) {
|
||||||
|
List<LoanDelinquencyActionData> effectiveDelinquencyList = calculateEffectiveDelinquencyList(savedDelinquencyActions);
|
||||||
|
LoanDelinquencyAction parsedDelinquencyAction = parseCommand(command);
|
||||||
|
validateLoanIsActive(loan);
|
||||||
|
if (DelinquencyAction.PAUSE.equals(parsedDelinquencyAction.getAction())) {
|
||||||
|
validatePauseStartAndEndDate(parsedDelinquencyAction, businessDate);
|
||||||
|
validatePauseShallNotOverlap(parsedDelinquencyAction, effectiveDelinquencyList);
|
||||||
|
} else if (DelinquencyAction.RESUME.equals(parsedDelinquencyAction.getAction())) {
|
||||||
|
validateResumeStartDate(parsedDelinquencyAction, businessDate);
|
||||||
|
validateResumeNoEndDate(parsedDelinquencyAction);
|
||||||
|
validateResumeShouldBeOnActivePause(parsedDelinquencyAction, effectiveDelinquencyList);
|
||||||
|
}
|
||||||
|
return parsedDelinquencyAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<LoanDelinquencyActionData> calculateEffectiveDelinquencyList(List<LoanDelinquencyAction> savedDelinquencyActions) {
|
||||||
|
// partition them based on type
|
||||||
|
Map<DelinquencyAction, List<LoanDelinquencyAction>> partitioned = savedDelinquencyActions.stream()
|
||||||
|
.collect(Collectors.groupingBy(LoanDelinquencyAction::getAction));
|
||||||
|
List<LoanDelinquencyActionData> effective = new ArrayList<>();
|
||||||
|
List<LoanDelinquencyAction> pauses = partitioned.get(DelinquencyAction.PAUSE);
|
||||||
|
if (pauses != null && pauses.size() > 0) {
|
||||||
|
for (LoanDelinquencyAction loanDelinquencyAction : pauses) {
|
||||||
|
Optional<LoanDelinquencyAction> resume = findMatchingResume(loanDelinquencyAction, partitioned.get(RESUME));
|
||||||
|
LoanDelinquencyActionData loanDelinquencyActionData = new LoanDelinquencyActionData(loanDelinquencyAction);
|
||||||
|
resume.ifPresent(r -> loanDelinquencyActionData.setEndDate(r.getStartDate()));
|
||||||
|
effective.add(loanDelinquencyActionData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return effective;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<LoanDelinquencyAction> findMatchingResume(LoanDelinquencyAction pause, List<LoanDelinquencyAction> resumes) {
|
||||||
|
if (resumes != null && resumes.size() > 0) {
|
||||||
|
for (LoanDelinquencyAction resume : resumes) {
|
||||||
|
if (!pause.getStartDate().isAfter(resume.getStartDate()) && !resume.getStartDate().isAfter(pause.getEndDate())) {
|
||||||
|
return Optional.of(resume);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateResumeShouldBeOnActivePause(LoanDelinquencyAction parsedDelinquencyAction,
|
||||||
|
List<LoanDelinquencyActionData> savedDelinquencyActions) {
|
||||||
|
boolean match = savedDelinquencyActions.stream()
|
||||||
|
.anyMatch(lda -> !DateUtils.isBefore(parsedDelinquencyAction.getStartDate(), lda.getStartDate())
|
||||||
|
&& !DateUtils.isAfter(parsedDelinquencyAction.getStartDate(), lda.getEndDate()));
|
||||||
|
if (!match) {
|
||||||
|
raiseValidationError("loan-delinquency-action-resume-should-be-on-pause",
|
||||||
|
"Resume Delinquency Action can only be created during an active pause");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateResumeNoEndDate(LoanDelinquencyAction parsedDelinquencyAction) {
|
||||||
|
if (parsedDelinquencyAction.getEndDate() != null) {
|
||||||
|
raiseValidationError("loan-delinquency-action-resume-should-have-no-end-date",
|
||||||
|
"Resume Delinquency action can not have end date");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateResumeStartDate(LoanDelinquencyAction parsedDelinquencyAction, LocalDate businessDate) {
|
||||||
|
if (!parsedDelinquencyAction.getStartDate().equals(businessDate)) {
|
||||||
|
raiseValidationError("loan-delinquency-action-invalid-start-date",
|
||||||
|
"Start date of the Resume Delinquency action must be the current business date");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePauseStartAndEndDate(LoanDelinquencyAction parsedDelinquencyAction, LocalDate businessDate) {
|
||||||
|
if (parsedDelinquencyAction.getStartDate().equals(parsedDelinquencyAction.getEndDate())) {
|
||||||
|
raiseValidationError("loan-delinquency-action-invalid-start-date-and-end-date",
|
||||||
|
"Delinquency pause period must be at least one day");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (businessDate.isAfter(parsedDelinquencyAction.getStartDate())) {
|
||||||
|
raiseValidationError("loan-delinquency-action-invalid-start-date", "Start date of pause period must be in the future");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateLoanIsActive(Loan loan) {
|
||||||
|
if (!loan.getStatus().isActive()) {
|
||||||
|
raiseValidationError("loan-delinquency-action-invalid-loan-state", "Delinquency actions can be created only for active loans.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validatePauseShallNotOverlap(LoanDelinquencyAction parsedDelinquencyAction,
|
||||||
|
List<LoanDelinquencyActionData> delinquencyActions) {
|
||||||
|
if (delinquencyActions.stream().filter(lda -> lda.getAction().equals(DelinquencyAction.PAUSE))
|
||||||
|
.anyMatch(lda -> isOverlapping(parsedDelinquencyAction, lda))) {
|
||||||
|
raiseValidationError("loan-delinquency-action-overlapping",
|
||||||
|
"Delinquency pause period cannot overlap with another pause period");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOverlapping(LoanDelinquencyAction parsedDelinquencyAction, LoanDelinquencyActionData ldad) {
|
||||||
|
return ((!parsedDelinquencyAction.getStartDate().isAfter(ldad.getStartDate())
|
||||||
|
&& !ldad.getStartDate().isAfter(parsedDelinquencyAction.getEndDate()))
|
||||||
|
|| (!parsedDelinquencyAction.getStartDate().isAfter(ldad.getEndDate())
|
||||||
|
&& !ldad.getEndDate().isAfter(parsedDelinquencyAction.getEndDate())));
|
||||||
|
}
|
||||||
|
|
||||||
|
@org.jetbrains.annotations.NotNull
|
||||||
|
private LoanDelinquencyAction parseCommand(@org.jetbrains.annotations.NotNull JsonCommand command) {
|
||||||
|
LoanDelinquencyAction parsedDelinquencyAction = new LoanDelinquencyAction();
|
||||||
|
parsedDelinquencyAction.setAction(extractAction(command.parsedJson()));
|
||||||
|
parsedDelinquencyAction.setStartDate(extractStartDate(command.parsedJson()));
|
||||||
|
parsedDelinquencyAction.setEndDate(extractEndDate(command.parsedJson()));
|
||||||
|
return parsedDelinquencyAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DelinquencyAction extractAction(JsonElement json) {
|
||||||
|
String actionString = jsonHelper.extractStringNamed(DelinquencyActionParameters.ACTION, json);
|
||||||
|
validateActionString(actionString);
|
||||||
|
if ("pause".equalsIgnoreCase(actionString)) {
|
||||||
|
return DelinquencyAction.PAUSE;
|
||||||
|
} else if ("resume".equalsIgnoreCase(actionString)) {
|
||||||
|
return DelinquencyAction.RESUME;
|
||||||
|
} else {
|
||||||
|
throw new PlatformApiDataValidationException(List.of(ApiParameterError.generalError("loan-delinquency-action-invalid-action",
|
||||||
|
"Invalid Delinquency Action: " + actionString)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateActionString(String actionString) {
|
||||||
|
if (StringUtils.isEmpty(actionString)) {
|
||||||
|
raiseValidationError("loan-delinquency-action-missing-action", "Delinquency Action must not be null or empty");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate extractStartDate(JsonElement json) {
|
||||||
|
String dateFormat = jsonHelper.extractStringNamed(DelinquencyActionParameters.DATE_FORMAT, json);
|
||||||
|
String locale = jsonHelper.extractStringNamed(DelinquencyActionParameters.LOCALE, json);
|
||||||
|
return jsonHelper.extractLocalDateNamed(DelinquencyActionParameters.START_DATE, json, dateFormat,
|
||||||
|
JsonParserHelper.localeFromString(locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate extractEndDate(JsonElement json) {
|
||||||
|
String dateFormat = jsonHelper.extractStringNamed(DelinquencyActionParameters.DATE_FORMAT, json);
|
||||||
|
String locale = jsonHelper.extractStringNamed(DelinquencyActionParameters.LOCALE, json);
|
||||||
|
return jsonHelper.extractLocalDateNamed(DelinquencyActionParameters.END_DATE, json, dateFormat,
|
||||||
|
JsonParserHelper.localeFromString(locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void raiseValidationError(String globalisationMessageCode, String msg) throws PlatformApiDataValidationException {
|
||||||
|
throw new PlatformApiDataValidationException(List.of(ApiParameterError.generalError(globalisationMessageCode, msg)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* 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.delinquency.validator;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class LoanDelinquencyActionData {
|
||||||
|
|
||||||
|
public Long id;
|
||||||
|
public DelinquencyAction action;
|
||||||
|
public LocalDate startDate;
|
||||||
|
public LocalDate endDate;
|
||||||
|
public Long createdById;
|
||||||
|
public OffsetDateTime createdOn;
|
||||||
|
public Long updatedById;
|
||||||
|
public OffsetDateTime lastModifiedOn;
|
||||||
|
|
||||||
|
public LoanDelinquencyActionData(LoanDelinquencyAction loanDelinquencyAction) {
|
||||||
|
this.id = loanDelinquencyAction.getId();
|
||||||
|
this.action = loanDelinquencyAction.getAction();
|
||||||
|
this.startDate = loanDelinquencyAction.getStartDate();
|
||||||
|
this.endDate = loanDelinquencyAction.getEndDate();
|
||||||
|
|
||||||
|
loanDelinquencyAction.getCreatedBy().ifPresent(this::setCreatedById);
|
||||||
|
loanDelinquencyAction.getLastModifiedBy().ifPresent(this::setUpdatedById);
|
||||||
|
this.createdOn = loanDelinquencyAction.getCreatedDateTime();
|
||||||
|
this.lastModifiedOn = loanDelinquencyAction.getLastModifiedDateTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+77
@@ -109,7 +109,9 @@ import org.apache.fineract.portfolio.collateralmanagement.service.LoanCollateral
|
|||||||
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
|
||||||
import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiResourceSwagger;
|
import org.apache.fineract.portfolio.delinquency.api.DelinquencyApiResourceSwagger;
|
||||||
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
|
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
|
||||||
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
|
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData;
|
||||||
import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData;
|
import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData;
|
||||||
import org.apache.fineract.portfolio.fund.data.FundData;
|
import org.apache.fineract.portfolio.fund.data.FundData;
|
||||||
import org.apache.fineract.portfolio.fund.service.FundReadPlatformService;
|
import org.apache.fineract.portfolio.fund.service.FundReadPlatformService;
|
||||||
@@ -257,6 +259,7 @@ public class LoansApiResource {
|
|||||||
private final DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer;
|
private final DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer;
|
||||||
private final DefaultToApiJsonSerializer<LoanApprovalData> loanApprovalDataToApiJsonSerializer;
|
private final DefaultToApiJsonSerializer<LoanApprovalData> loanApprovalDataToApiJsonSerializer;
|
||||||
private final DefaultToApiJsonSerializer<LoanScheduleData> loanScheduleToApiJsonSerializer;
|
private final DefaultToApiJsonSerializer<LoanScheduleData> loanScheduleToApiJsonSerializer;
|
||||||
|
private final DefaultToApiJsonSerializer<LoanDelinquencyActionData> delinquencyActionSerializer;
|
||||||
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||||
private final FromJsonHelper fromJsonHelper;
|
private final FromJsonHelper fromJsonHelper;
|
||||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||||
@@ -778,6 +781,58 @@ public class LoansApiResource {
|
|||||||
return getDelinquencyTagHistory(null, loanExternalId, uriInfo);
|
return getDelinquencyTagHistory(null, loanExternalId, uriInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("{loanId}/delinquency-actions")
|
||||||
|
@Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Operation(summary = "Retrieve delinquency actions related to the loan", description = "")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DelinquencyApiResourceSwagger.GetDelinquencyActionsResponse.class)))) })
|
||||||
|
public String getLoanDelinquencyActions(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
|
||||||
|
@Context final UriInfo uriInfo) {
|
||||||
|
return getLoanDelinquencyActions(loanId, null, uriInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("external-id/{loanExternalId}/delinquency-actions")
|
||||||
|
@Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Operation(summary = "Retrieve delinquency actions related to the loan", description = "")
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = DelinquencyApiResourceSwagger.GetDelinquencyActionsResponse.class)))) })
|
||||||
|
public String getLoanDelinquencyActions(
|
||||||
|
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
|
||||||
|
@Context final UriInfo uriInfo) {
|
||||||
|
return getLoanDelinquencyActions(null, loanExternalId, uriInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("{loanId}/delinquency-actions")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Operation(summary = "Adds a new delinquency action for a loan", description = "")
|
||||||
|
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostLoansDelinquencyActionRequest.class)))
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostLoansDelinquencyActionResponse.class))) })
|
||||||
|
public String createLoanDelinquencyAction(@PathParam("loanId") @Parameter(description = "loanId", required = true) final Long loanId,
|
||||||
|
@Context final UriInfo uriInfo, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
return createLoanDelinquencyAction(loanId, ExternalId.empty(), apiRequestBodyAsJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("external-id/{loanExternalId}/delinquency-actions")
|
||||||
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
|
@Produces(MediaType.APPLICATION_JSON)
|
||||||
|
@Operation(summary = "Adds a new delinquency action for a loan", description = "")
|
||||||
|
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostLoansDelinquencyActionRequest.class)))
|
||||||
|
@ApiResponses({
|
||||||
|
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = DelinquencyApiResourceSwagger.PostLoansDelinquencyActionResponse.class))) })
|
||||||
|
public String createLoanDelinquencyAction(
|
||||||
|
@PathParam("loanExternalId") @Parameter(description = "loanExternalId", required = true) final String loanExternalId,
|
||||||
|
@Context final UriInfo uriInfo, @Parameter(hidden = true) final String apiRequestBodyAsJson) {
|
||||||
|
return createLoanDelinquencyAction(null, ExternalIdFactory.produce(loanExternalId), apiRequestBodyAsJson);
|
||||||
|
}
|
||||||
|
|
||||||
private String retrieveApprovalTemplate(final Long loanId, final String loanExternalIdStr, final String templateType,
|
private String retrieveApprovalTemplate(final Long loanId, final String loanExternalIdStr, final String templateType,
|
||||||
final UriInfo uriInfo) {
|
final UriInfo uriInfo) {
|
||||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
@@ -1154,4 +1209,26 @@ public class LoansApiResource {
|
|||||||
}
|
}
|
||||||
return resolvedLoanId;
|
return resolvedLoanId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String getLoanDelinquencyActions(final Long loanId, final String loanExternalIdStr, final UriInfo uriInfo) {
|
||||||
|
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
ExternalId loanExternalId = ExternalIdFactory.produce(loanExternalIdStr);
|
||||||
|
Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId);
|
||||||
|
|
||||||
|
final Collection<LoanDelinquencyAction> delinquencyActions = this.delinquencyReadPlatformService
|
||||||
|
.retrieveLoanDelinquencyActions(resolvedLoanId);
|
||||||
|
List<LoanDelinquencyActionData> result = delinquencyActions.stream().map(LoanDelinquencyActionData::new).toList();
|
||||||
|
return this.jsonSerializerTagHistory.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createLoanDelinquencyAction(Long loanId, ExternalId loanExternalId, String apiRequestBodyAsJson) {
|
||||||
|
context.authenticatedUser().validateHasUpdatePermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||||
|
Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId);
|
||||||
|
|
||||||
|
CommandWrapperBuilder builder = new CommandWrapperBuilder().createDelinquencyAction(resolvedLoanId);
|
||||||
|
builder.withJson(apiRequestBodyAsJson);
|
||||||
|
CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(builder.build());
|
||||||
|
return delinquencyActionSerializer.serialize(result);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -964,9 +964,9 @@ final class LoansApiResourceSwagger {
|
|||||||
private String externalId;
|
private String externalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
static final class GetLoansLoanIdCollectionData {
|
static final class GetLoansLoanIdDelinquencySummary {
|
||||||
|
|
||||||
private GetLoansLoanIdCollectionData() {}
|
private GetLoansLoanIdDelinquencySummary() {}
|
||||||
|
|
||||||
@Schema(example = "100.000000")
|
@Schema(example = "100.000000")
|
||||||
public Double availableDisbursementAmount;
|
public Double availableDisbursementAmount;
|
||||||
@@ -989,6 +989,16 @@ final class LoansApiResourceSwagger {
|
|||||||
public LocalDate lastRepaymentDate;
|
public LocalDate lastRepaymentDate;
|
||||||
@Schema(example = "100.000000")
|
@Schema(example = "100.000000")
|
||||||
public Double lastRepaymentAmount;
|
public Double lastRepaymentAmount;
|
||||||
|
|
||||||
|
@Schema(example = "true")
|
||||||
|
public Boolean delinquencyCalculationPaused;
|
||||||
|
|
||||||
|
@Schema(example = "[2022, 07, 05]")
|
||||||
|
public LocalDate delinquencyPausePeriodStartDate;
|
||||||
|
|
||||||
|
@Schema(example = "[2022, 07, 10]")
|
||||||
|
public LocalDate delinquencyPausePeriodEndDate;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Schema(example = "1")
|
@Schema(example = "1")
|
||||||
@@ -1062,7 +1072,7 @@ final class LoansApiResourceSwagger {
|
|||||||
@Schema(description = "Set of GetLoansLoanIdDisbursementDetails")
|
@Schema(description = "Set of GetLoansLoanIdDisbursementDetails")
|
||||||
public Set<GetLoansLoanIdDisbursementDetails> disbursementDetails;
|
public Set<GetLoansLoanIdDisbursementDetails> disbursementDetails;
|
||||||
@Schema(description = "Delinquent data")
|
@Schema(description = "Delinquent data")
|
||||||
public GetLoansLoanIdCollectionData delinquent;
|
public GetLoansLoanIdDelinquencySummary delinquent;
|
||||||
@Schema(description = "Set of charges")
|
@Schema(description = "Set of charges")
|
||||||
public List<GetLoansLoanIdLoanChargeData> charges;
|
public List<GetLoansLoanIdLoanChargeData> charges;
|
||||||
public GetDelinquencyRangesResponse delinquencyRange;
|
public GetDelinquencyRangesResponse delinquencyRange;
|
||||||
|
|||||||
+5
-1
@@ -43,9 +43,13 @@ public final class CollectionData {
|
|||||||
private LocalDate lastRepaymentDate;
|
private LocalDate lastRepaymentDate;
|
||||||
private BigDecimal lastRepaymentAmount;
|
private BigDecimal lastRepaymentAmount;
|
||||||
|
|
||||||
|
public boolean delinquencyCalculationPaused;
|
||||||
|
public LocalDate delinquencyPausePeriodStartDate;
|
||||||
|
public LocalDate delinquencyPausePeriodEndDate;
|
||||||
|
|
||||||
public static CollectionData template() {
|
public static CollectionData template() {
|
||||||
final BigDecimal zero = BigDecimal.ZERO;
|
final BigDecimal zero = BigDecimal.ZERO;
|
||||||
return new CollectionData(zero, 0L, null, 0L, null, zero, null, zero, null, zero);
|
return new CollectionData(zero, 0L, null, 0L, null, zero, null, zero, null, zero, false, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+204
@@ -0,0 +1,204 @@
|
|||||||
|
/**
|
||||||
|
* 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.delinquency.service;
|
||||||
|
|
||||||
|
import static java.time.Month.JANUARY;
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.PAUSE;
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.RESUME;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.mapper.DelinquencyRangeMapper;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.mapper.LoanDelinquencyTagMapper;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class DelinquencyReadPlatformServiceImplTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DelinquencyRangeRepository repositoryRange;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private DelinquencyBucketRepository repositoryBucket;
|
||||||
|
@Mock
|
||||||
|
private LoanDelinquencyTagHistoryRepository repositoryLoanDelinquencyTagHistory;
|
||||||
|
@Mock
|
||||||
|
private DelinquencyRangeMapper mapperRange;
|
||||||
|
@Mock
|
||||||
|
private DelinquencyBucketMapper mapperBucket;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LoanRepository loanRepository;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LoanDelinquencyDomainService loanDelinquencyDomainService;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private LoanDelinquencyActionRepository loanDelinquencyActionRepository;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private DelinquencyReadPlatformServiceImpl underTest;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNoEnrichmentWhenThereIsNoDelinquencyAction() {
|
||||||
|
// given
|
||||||
|
CollectionData collectionData = CollectionData.template();
|
||||||
|
Collection<LoanDelinquencyAction> delinquencyActions = List.of();
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.enrichWithDelinquencyPausePeriodInfo(collectionData, delinquencyActions, LocalDate.of(2023, JANUARY, 12));
|
||||||
|
|
||||||
|
// then
|
||||||
|
Assertions.assertFalse(collectionData.isDelinquencyCalculationPaused());
|
||||||
|
Assertions.assertNull(collectionData.getDelinquencyPausePeriodStartDate());
|
||||||
|
Assertions.assertNull(collectionData.getDelinquencyPausePeriodEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiplePausesWithoutResumeActionCurrentlyInPauseFirstDay() {
|
||||||
|
// given
|
||||||
|
CollectionData collectionData = CollectionData.template();
|
||||||
|
Collection<LoanDelinquencyAction> delinquencyActions = List.of(
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 10), LocalDate.of(2023, JANUARY, 11)),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 12), LocalDate.of(2023, JANUARY, 13)),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.enrichWithDelinquencyPausePeriodInfo(collectionData, delinquencyActions, LocalDate.of(2023, JANUARY, 12));
|
||||||
|
|
||||||
|
// then
|
||||||
|
Assertions.assertTrue(collectionData.isDelinquencyCalculationPaused());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 12), collectionData.getDelinquencyPausePeriodStartDate());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 13), collectionData.getDelinquencyPausePeriodEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiplePausesWithoutResumeActionCurrentlyInPauseLastDay() {
|
||||||
|
// given
|
||||||
|
CollectionData collectionData = CollectionData.template();
|
||||||
|
Collection<LoanDelinquencyAction> delinquencyActions = List.of(
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 10), LocalDate.of(2023, JANUARY, 11)),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 12), LocalDate.of(2023, JANUARY, 13)),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.enrichWithDelinquencyPausePeriodInfo(collectionData, delinquencyActions, LocalDate.of(2023, JANUARY, 13));
|
||||||
|
|
||||||
|
// then
|
||||||
|
Assertions.assertTrue(collectionData.isDelinquencyCalculationPaused());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 12), collectionData.getDelinquencyPausePeriodStartDate());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 13), collectionData.getDelinquencyPausePeriodEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiplePausesWithoutResumeActionCurrentBusinessDateBetweenStartAndEndDate() {
|
||||||
|
// given
|
||||||
|
CollectionData collectionData = CollectionData.template();
|
||||||
|
Collection<LoanDelinquencyAction> delinquencyActions = List.of(
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 10), LocalDate.of(2023, JANUARY, 11)),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 12), LocalDate.of(2023, JANUARY, 14)),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.enrichWithDelinquencyPausePeriodInfo(collectionData, delinquencyActions, LocalDate.of(2023, JANUARY, 13));
|
||||||
|
|
||||||
|
// then
|
||||||
|
Assertions.assertTrue(collectionData.isDelinquencyCalculationPaused());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 12), collectionData.getDelinquencyPausePeriodStartDate());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 14), collectionData.getDelinquencyPausePeriodEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultiplePausesWithoutResumeCurrentBusinessDateIsNotOverlappingWithAnyOfThePauses() {
|
||||||
|
// given
|
||||||
|
CollectionData collectionData = CollectionData.template();
|
||||||
|
Collection<LoanDelinquencyAction> delinquencyActions = List.of(
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 10), LocalDate.of(2023, JANUARY, 11)),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 13), LocalDate.of(2023, JANUARY, 14)),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.enrichWithDelinquencyPausePeriodInfo(collectionData, delinquencyActions, LocalDate.of(2023, JANUARY, 12));
|
||||||
|
|
||||||
|
// then
|
||||||
|
Assertions.assertFalse(collectionData.isDelinquencyCalculationPaused());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 10), collectionData.getDelinquencyPausePeriodStartDate());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 11), collectionData.getDelinquencyPausePeriodEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResumeIsAppliedToOneOfThePauseNotActive() {
|
||||||
|
// given
|
||||||
|
CollectionData collectionData = CollectionData.template();
|
||||||
|
Collection<LoanDelinquencyAction> delinquencyActions = List.of(
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 10), LocalDate.of(2023, JANUARY, 20)),
|
||||||
|
new LoanDelinquencyAction(null, RESUME, LocalDate.of(2023, JANUARY, 11), null),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 13), LocalDate.of(2023, JANUARY, 14)),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.enrichWithDelinquencyPausePeriodInfo(collectionData, delinquencyActions, LocalDate.of(2023, JANUARY, 12));
|
||||||
|
|
||||||
|
// then
|
||||||
|
Assertions.assertFalse(collectionData.isDelinquencyCalculationPaused());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 10), collectionData.getDelinquencyPausePeriodStartDate());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 11), collectionData.getDelinquencyPausePeriodEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResumeIsAppliedToOneOfThePauseActive() {
|
||||||
|
// given
|
||||||
|
CollectionData collectionData = CollectionData.template();
|
||||||
|
Collection<LoanDelinquencyAction> delinquencyActions = List.of(
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 10), LocalDate.of(2023, JANUARY, 20)),
|
||||||
|
new LoanDelinquencyAction(null, RESUME, LocalDate.of(2023, JANUARY, 11), null),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 13), LocalDate.of(2023, JANUARY, 14)),
|
||||||
|
new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023, JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
|
||||||
|
|
||||||
|
// when
|
||||||
|
underTest.enrichWithDelinquencyPausePeriodInfo(collectionData, delinquencyActions, LocalDate.of(2023, JANUARY, 11));
|
||||||
|
|
||||||
|
// then
|
||||||
|
Assertions.assertTrue(collectionData.isDelinquencyCalculationPaused());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 10), collectionData.getDelinquencyPausePeriodStartDate());
|
||||||
|
Assertions.assertEquals(LocalDate.of(2023, JANUARY, 11), collectionData.getDelinquencyPausePeriodEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+293
@@ -0,0 +1,293 @@
|
|||||||
|
/**
|
||||||
|
* 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.delinquency.validator;
|
||||||
|
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.PAUSE;
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.RESUME;
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParameters.ACTION;
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParameters.DATE_FORMAT;
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParameters.END_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParameters.LOCALE;
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParameters.START_DATE;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.google.gson.JsonParser;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.apache.fineract.infrastructure.core.api.JsonCommand;
|
||||||
|
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
|
||||||
|
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.function.Executable;
|
||||||
|
import org.mockito.Mockito;
|
||||||
|
|
||||||
|
class DelinquencyActionParseAndValidatorTest {
|
||||||
|
|
||||||
|
private final FromJsonHelper fromJsonHelper = new FromJsonHelper();
|
||||||
|
private final DelinquencyActionParseAndValidator underTest = new DelinquencyActionParseAndValidator(fromJsonHelper);
|
||||||
|
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd MMMM yyyy", Locale.US);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseAndValidationIsOKForPause() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
JsonCommand command = delinquencyAction("pause", "09 September 2022", "19 September 2022");
|
||||||
|
|
||||||
|
LoanDelinquencyAction parsedDelinquencyAction = underTest.validateAndParseUpdate(command, loan, List.of(),
|
||||||
|
localDate("09 September 2022"));
|
||||||
|
Assertions.assertEquals(PAUSE, parsedDelinquencyAction.getAction());
|
||||||
|
Assertions.assertEquals(localDate("09 September 2022"), parsedDelinquencyAction.getStartDate());
|
||||||
|
Assertions.assertEquals(localDate("19 September 2022"), parsedDelinquencyAction.getEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testParseAndValidationIsOKForResume() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
JsonCommand command = delinquencyAction("resume", "09 September 2022", null);
|
||||||
|
|
||||||
|
LoanDelinquencyAction parsedDelinquencyAction = underTest.validateAndParseUpdate(command, loan,
|
||||||
|
List.of(loanDelinquencyAction(PAUSE, "05 September 2022", "15 September 2022")), localDate("09 September 2022"));
|
||||||
|
Assertions.assertEquals(DelinquencyAction.RESUME, parsedDelinquencyAction.getAction());
|
||||||
|
Assertions.assertEquals(localDate("09 September 2022"), parsedDelinquencyAction.getStartDate());
|
||||||
|
Assertions.assertNull(parsedDelinquencyAction.getEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPauseBothStartAndEndDateIsOverlappingWithAnActivePause() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "14 September 2022", "22 September 2022"));
|
||||||
|
JsonCommand command = delinquencyAction("pause", "09 September 2022", "15 September 2022");
|
||||||
|
|
||||||
|
assertPlatformValidationException("Delinquency pause period cannot overlap with another pause period",
|
||||||
|
"loan-delinquency-action-overlapping",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, existing, localDate("09 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testPauseStartIsOverlappingWithAnActivePause() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "14 September 2022", "22 September 2022"));
|
||||||
|
JsonCommand command = delinquencyAction("pause", "15 September 2022", "23 September 2022");
|
||||||
|
|
||||||
|
assertPlatformValidationException("Delinquency pause period cannot overlap with another pause period",
|
||||||
|
"loan-delinquency-action-overlapping",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, existing, localDate("09 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNewPauseEndIsTheSameAsExistingPauseStart() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "15 September 2022", "22 September 2022"));
|
||||||
|
JsonCommand command = delinquencyAction("pause", "09 September 2022", "15 September 2022");
|
||||||
|
|
||||||
|
assertPlatformValidationException("Delinquency pause period cannot overlap with another pause period",
|
||||||
|
"loan-delinquency-action-overlapping",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, existing, localDate("09 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNewPauseEndIsTheSameAsExistingPauseEnd() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "15 September 2022", "22 September 2022"));
|
||||||
|
JsonCommand command = delinquencyAction("pause", "22 September 2022", "25 September 2022");
|
||||||
|
|
||||||
|
assertPlatformValidationException("Delinquency pause period cannot overlap with another pause period",
|
||||||
|
"loan-delinquency-action-overlapping",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, existing, localDate("09 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNewPauseEndIsOverlappingWithExistingPause() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "15 September 2022", "22 September 2022"));
|
||||||
|
JsonCommand command = delinquencyAction("pause", "13 September 2022", "20 September 2022");
|
||||||
|
|
||||||
|
assertPlatformValidationException("Delinquency pause period cannot overlap with another pause period",
|
||||||
|
"loan-delinquency-action-overlapping",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, existing, localDate("09 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testNewPauseIsNotOverlappingBecauseThereWasAResume() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
JsonCommand command = delinquencyAction("pause", "18 September 2022", "20 September 2022");
|
||||||
|
|
||||||
|
List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "15 September 2022", "22 September 2022"), //
|
||||||
|
loanDelinquencyAction(RESUME, "17 September 2022") //
|
||||||
|
);
|
||||||
|
|
||||||
|
LoanDelinquencyAction parsedDelinquencyAction = underTest.validateAndParseUpdate(command, loan, existing,
|
||||||
|
localDate("18 September 2022"));
|
||||||
|
Assertions.assertEquals(PAUSE, parsedDelinquencyAction.getAction());
|
||||||
|
Assertions.assertEquals(localDate("18 September 2022"), parsedDelinquencyAction.getStartDate());
|
||||||
|
Assertions.assertEquals(localDate("20 September 2022"), parsedDelinquencyAction.getEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testResumeIsNotOverlappingWithAnActivePause() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "05 September 2022", "08 September 2022"));
|
||||||
|
JsonCommand command = delinquencyAction("resume", "09 September 2022", null);
|
||||||
|
|
||||||
|
assertPlatformValidationException("Resume Delinquency Action can only be created during an active pause",
|
||||||
|
"loan-delinquency-action-resume-should-be-on-pause",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, existing, localDate("09 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationErrorWhenDelinquencyActionIsMissing() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.APPROVED);
|
||||||
|
|
||||||
|
JsonCommand command = delinquencyAction(null, "09 September 2022", "19 September 2022");
|
||||||
|
|
||||||
|
assertPlatformValidationException("Delinquency Action must not be null or empty", "loan-delinquency-action-missing-action",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, List.of(), localDate("09 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationErrorWhenLoanIsNotActive() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.APPROVED);
|
||||||
|
|
||||||
|
JsonCommand command = delinquencyAction("pause", "09 September 2022", "19 September 2022");
|
||||||
|
|
||||||
|
assertPlatformValidationException("Delinquency actions can be created only for active loans.",
|
||||||
|
"loan-delinquency-action-invalid-loan-state",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, List.of(), localDate("09 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationErrorResumeShouldHaveNoEndDate() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
JsonCommand command = delinquencyAction("resume", "09 September 2022", "19 September 2022");
|
||||||
|
|
||||||
|
assertPlatformValidationException("Resume Delinquency action can not have end date",
|
||||||
|
"loan-delinquency-action-resume-should-have-no-end-date",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, List.of(), localDate("09 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationErrorResumeInvalidStartDate() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
JsonCommand command = delinquencyAction("resume", "09 September 2022", "19 September 2022");
|
||||||
|
|
||||||
|
assertPlatformValidationException("Start date of the Resume Delinquency action must be the current business date",
|
||||||
|
"loan-delinquency-action-invalid-start-date",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, List.of(), localDate("10 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationErrorPausePeriodShouldBeAtLeastOneDay() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
JsonCommand command = delinquencyAction("pause", "10 September 2022", "10 September 2022");
|
||||||
|
|
||||||
|
assertPlatformValidationException("Delinquency pause period must be at least one day",
|
||||||
|
"loan-delinquency-action-invalid-start-date-and-end-date",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, List.of(), localDate("09 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationErrorPausePeriodMustBeInFuture() throws JsonProcessingException {
|
||||||
|
Loan loan = Mockito.mock(Loan.class);
|
||||||
|
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
|
||||||
|
|
||||||
|
JsonCommand command = delinquencyAction("pause", "08 September 2022", "09 September 2022");
|
||||||
|
|
||||||
|
assertPlatformValidationException("Start date of pause period must be in the future", "loan-delinquency-action-invalid-start-date",
|
||||||
|
() -> underTest.validateAndParseUpdate(command, loan, List.of(), localDate("09 September 2022")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private JsonCommand delinquencyAction(String action, String startDate, String endDate) throws JsonProcessingException {
|
||||||
|
Map<String, Object> map = new HashMap<>();
|
||||||
|
map.put(ACTION, action);
|
||||||
|
map.put(DATE_FORMAT, "dd MMMM yyyy");
|
||||||
|
map.put(LOCALE, "en");
|
||||||
|
map.put(START_DATE, startDate);
|
||||||
|
map.put(END_DATE, endDate);
|
||||||
|
return createJsonCommand(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDate localDate(String date) {
|
||||||
|
return LocalDate.parse(date, DATE_TIME_FORMATTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private JsonCommand createJsonCommand(Map<String, Object> jsonMap) throws JsonProcessingException {
|
||||||
|
ObjectMapper objectMapper = new ObjectMapper();
|
||||||
|
String json = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonMap);
|
||||||
|
return new JsonCommand(null, JsonParser.parseString(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertPlatformValidationException(String message, String code, Executable executable) {
|
||||||
|
PlatformApiDataValidationException validationException = assertThrows(PlatformApiDataValidationException.class, executable);
|
||||||
|
assertPlatformException(message, code, validationException);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertPlatformException(String expectedMessage, String expectedCode,
|
||||||
|
PlatformApiDataValidationException platformApiDataValidationException) {
|
||||||
|
Assertions.assertEquals(expectedMessage, platformApiDataValidationException.getErrors().get(0).getDefaultUserMessage());
|
||||||
|
Assertions.assertEquals(expectedCode, platformApiDataValidationException.getErrors().get(0).getUserMessageGlobalisationCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoanDelinquencyAction loanDelinquencyAction(DelinquencyAction action, String startTime, String endTime) {
|
||||||
|
return new LoanDelinquencyAction(null, action, localDate(startTime), localDate(endTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
private LoanDelinquencyAction loanDelinquencyAction(DelinquencyAction action, String startTime) {
|
||||||
|
return new LoanDelinquencyAction(null, action, localDate(startTime), null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+5
-5
@@ -127,7 +127,7 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||||
loanForProcessing);
|
loanForProcessing);
|
||||||
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
|
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
|
||||||
null, null);
|
null, null, false, null, null);
|
||||||
|
|
||||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||||
|
|
||||||
@@ -239,10 +239,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||||
loanForProcessing);
|
loanForProcessing);
|
||||||
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
|
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
|
||||||
null, null);
|
null, null, false, null, null);
|
||||||
|
|
||||||
CollectionData installmentCollectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate,
|
CollectionData installmentCollectionData = new CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate,
|
||||||
installmentPrincipalAmount, null, null, null, null);
|
installmentPrincipalAmount, null, null, null, null, false, null, null);
|
||||||
|
|
||||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||||
installmentsCollection.put(1L, installmentCollectionData);
|
installmentsCollection.put(1L, installmentCollectionData);
|
||||||
@@ -305,10 +305,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
|||||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||||
loanForProcessing);
|
loanForProcessing);
|
||||||
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate, BigDecimal.ZERO, null, null,
|
CollectionData collectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate, BigDecimal.ZERO, null, null,
|
||||||
null, null);
|
null, null, false, null, null);
|
||||||
|
|
||||||
CollectionData installmentCollectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate,
|
CollectionData installmentCollectionData = new CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate,
|
||||||
installmentPrincipalAmount, null, null, null, null);
|
installmentPrincipalAmount, null, null, null, null, false, null, null);
|
||||||
|
|
||||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||||
installmentsCollection.put(1L, installmentCollectionData);
|
installmentsCollection.put(1L, installmentCollectionData);
|
||||||
|
|||||||
+8
-3
@@ -79,6 +79,7 @@ public abstract class BaseLoanIntegrationTest {
|
|||||||
protected final LoanProductHelper loanProductHelper = new LoanProductHelper();
|
protected final LoanProductHelper loanProductHelper = new LoanProductHelper();
|
||||||
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
|
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
|
||||||
protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
|
protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
|
||||||
|
protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
|
||||||
|
|
||||||
// asset
|
// asset
|
||||||
protected final Account loansReceivableAccount = accountHelper.createAssetAccount();
|
protected final Account loansReceivableAccount = accountHelper.createAssetAccount();
|
||||||
@@ -229,7 +230,6 @@ public abstract class BaseLoanIntegrationTest {
|
|||||||
assertNull(loanDetails.getTransactions(), "No transaction is expected");
|
assertNull(loanDetails.getTransactions(), "No transaction is expected");
|
||||||
} else {
|
} else {
|
||||||
Assertions.assertEquals(transactions.length, loanDetails.getTransactions().size());
|
Assertions.assertEquals(transactions.length, loanDetails.getTransactions().size());
|
||||||
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
|
|
||||||
Arrays.stream(transactions).forEach(tr -> {
|
Arrays.stream(transactions).forEach(tr -> {
|
||||||
boolean found = loanDetails.getTransactions().stream()
|
boolean found = loanDetails.getTransactions().stream()
|
||||||
.anyMatch(item -> Objects.equals(item.getAmount(), tr.amount) && Objects.equals(item.getType().getValue(), tr.type)
|
.anyMatch(item -> Objects.equals(item.getAmount(), tr.amount) && Objects.equals(item.getType().getValue(), tr.type)
|
||||||
@@ -329,8 +329,13 @@ public abstract class BaseLoanIntegrationTest {
|
|||||||
|
|
||||||
protected Long applyAndApproveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
|
protected Long applyAndApproveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
|
||||||
int numberOfRepayments) {
|
int numberOfRepayments) {
|
||||||
PostLoansResponse postLoansResponse = loanTransactionHelper
|
return applyAndApproveLoan(clientId, loanProductId, loanDisbursementDate, amount, numberOfRepayments, null);
|
||||||
.applyLoan(applyLoanRequest(clientId, loanProductId, loanDisbursementDate, amount, numberOfRepayments));
|
}
|
||||||
|
|
||||||
|
protected Long applyAndApproveLoan(Long clientId, Long loanProductId, String loanDisbursementDate, Double amount,
|
||||||
|
int numberOfRepayments, String externalId) {
|
||||||
|
PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(
|
||||||
|
applyLoanRequest(clientId, loanProductId, loanDisbursementDate, amount, numberOfRepayments).externalId(externalId));
|
||||||
|
|
||||||
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
|
||||||
approveLoanRequest(amount));
|
approveLoanRequest(amount));
|
||||||
|
|||||||
+259
@@ -0,0 +1,259 @@
|
|||||||
|
/**
|
||||||
|
* 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.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.PAUSE;
|
||||||
|
import static org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction.RESUME;
|
||||||
|
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;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.fineract.client.models.BusinessDateRequest;
|
||||||
|
import org.apache.fineract.client.models.GetDelinquencyActionsResponse;
|
||||||
|
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
|
||||||
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansDelinquencyActionResponse;
|
||||||
|
import org.apache.fineract.client.util.CallFailedRuntimeException;
|
||||||
|
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||||
|
import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
|
||||||
|
import org.junit.jupiter.api.Assertions;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@ExtendWith(LoanTestLifecycleExtension.class)
|
||||||
|
public class DelinquencyActionIntegrationTests extends BaseLoanIntegrationTest {
|
||||||
|
|
||||||
|
public static final BigDecimal DOWN_PAYMENT_PERCENTAGE = new BigDecimal(25);
|
||||||
|
private final ClientHelper clientHelper = new ClientHelper(this.requestSpec, this.responseSpec);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateAndReadPauseDelinquencyAction() {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Disburse Loan
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January 2023");
|
||||||
|
|
||||||
|
// Create Delinquency Pause for the Loan
|
||||||
|
PostLoansDelinquencyActionResponse response = loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE,
|
||||||
|
"10 January 2023", "15 January 2023");
|
||||||
|
|
||||||
|
List<GetDelinquencyActionsResponse> loanDelinquencyActions = loanTransactionHelper.getLoanDelinquencyActions(loanId);
|
||||||
|
Assertions.assertNotNull(loanDelinquencyActions);
|
||||||
|
Assertions.assertEquals(1, loanDelinquencyActions.size());
|
||||||
|
Assertions.assertEquals("PAUSE", loanDelinquencyActions.get(0).getAction());
|
||||||
|
Assertions.assertEquals(LocalDate.parse("10 January 2023", dateTimeFormatter), loanDelinquencyActions.get(0).getStartDate());
|
||||||
|
Assertions.assertEquals(LocalDate.parse("15 January 2023", dateTimeFormatter), loanDelinquencyActions.get(0).getEndDate());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreateAndReadPauseDelinquencyActionUsingExternalId() {
|
||||||
|
runAt("01 January 2023", () -> {
|
||||||
|
// Create Client
|
||||||
|
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||||
|
|
||||||
|
// Create Loan Product
|
||||||
|
Long loanProductId = createLoanProductWith25PctDownPayment(true, true);
|
||||||
|
|
||||||
|
// Create external ID
|
||||||
|
String externalId = UUID.randomUUID().toString();
|
||||||
|
|
||||||
|
// Apply and Approve Loan
|
||||||
|
Long loanId = applyAndApproveLoan(clientId, loanProductId, "01 January 2023", 1500.0, 2, externalId);
|
||||||
|
|
||||||
|
// Disburse Loan
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January 2023");
|
||||||
|
|
||||||
|
// Create Delinquency Pause for the Loan
|
||||||
|
PostLoansDelinquencyActionResponse response = loanTransactionHelper.createLoanDelinquencyAction(externalId, PAUSE,
|
||||||
|
"10 January 2023", "15 January 2023");
|
||||||
|
|
||||||
|
List<GetDelinquencyActionsResponse> loanDelinquencyActions = loanTransactionHelper.getLoanDelinquencyActions(externalId);
|
||||||
|
Assertions.assertNotNull(loanDelinquencyActions);
|
||||||
|
Assertions.assertEquals(1, loanDelinquencyActions.size());
|
||||||
|
Assertions.assertEquals("PAUSE", loanDelinquencyActions.get(0).getAction());
|
||||||
|
Assertions.assertEquals(LocalDate.parse("10 January 2023", dateTimeFormatter), loanDelinquencyActions.get(0).getStartDate());
|
||||||
|
Assertions.assertEquals(LocalDate.parse("15 January 2023", dateTimeFormatter), loanDelinquencyActions.get(0).getEndDate());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreatePauseAndResumeDelinquencyAction() {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Disburse Loan
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January 2023");
|
||||||
|
|
||||||
|
// Create Delinquency Pause for the Loan
|
||||||
|
loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, "10 January 2023", "15 January 2023");
|
||||||
|
|
||||||
|
// Update business date
|
||||||
|
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BUSINESS_DATE.getName()).date("14 January 2023")
|
||||||
|
.dateFormat(DATETIME_PATTERN).locale("en"));
|
||||||
|
|
||||||
|
// Create 2nd Delinquency Resume for the Loan
|
||||||
|
loanTransactionHelper.createLoanDelinquencyAction(loanId, RESUME, "14 January 2023");
|
||||||
|
|
||||||
|
List<GetDelinquencyActionsResponse> loanDelinquencyActions = loanTransactionHelper.getLoanDelinquencyActions(loanId);
|
||||||
|
Assertions.assertNotNull(loanDelinquencyActions);
|
||||||
|
Assertions.assertEquals(2, loanDelinquencyActions.size());
|
||||||
|
|
||||||
|
Assertions.assertEquals("PAUSE", loanDelinquencyActions.get(0).getAction());
|
||||||
|
Assertions.assertEquals(LocalDate.parse("10 January 2023", dateTimeFormatter), loanDelinquencyActions.get(0).getStartDate());
|
||||||
|
Assertions.assertEquals(LocalDate.parse("15 January 2023", dateTimeFormatter), loanDelinquencyActions.get(0).getEndDate());
|
||||||
|
|
||||||
|
Assertions.assertEquals("RESUME", loanDelinquencyActions.get(1).getAction());
|
||||||
|
Assertions.assertEquals(LocalDate.parse("14 January 2023", dateTimeFormatter), loanDelinquencyActions.get(1).getStartDate());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCreatePauseAndResumeDelinquencyActionWithStatusFlag() {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Disburse Loan
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January 2023");
|
||||||
|
|
||||||
|
// Create Delinquency Pause for the Loan
|
||||||
|
loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, "10 January 2023", "15 January 2023");
|
||||||
|
|
||||||
|
// Update business date
|
||||||
|
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BUSINESS_DATE.getName()).date("14 January 2023")
|
||||||
|
.dateFormat(DATETIME_PATTERN).locale("en"));
|
||||||
|
|
||||||
|
// Validate Loan Delinquency Pause Period on Loan
|
||||||
|
validateLoanDelinquencyPausePerios(loanId, "10 January 2023", "15 January 2023", true);
|
||||||
|
|
||||||
|
// Create a Resume for the Loan for the current business date, it is still expected to be in pause
|
||||||
|
loanTransactionHelper.createLoanDelinquencyAction(loanId, RESUME, "14 January 2023");
|
||||||
|
|
||||||
|
// Validate Loan Delinquency Pause Period on Loan
|
||||||
|
validateLoanDelinquencyPausePerios(loanId, "10 January 2023", "14 January 2023", true);
|
||||||
|
|
||||||
|
// Update business date to 15 January 2023
|
||||||
|
businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BUSINESS_DATE.getName()).date("15 January 2023")
|
||||||
|
.dateFormat(DATETIME_PATTERN).locale("en"));
|
||||||
|
|
||||||
|
// Validate Loan Delinquency Pause Period on Loan
|
||||||
|
validateLoanDelinquencyPausePerios(loanId, "10 January 2023", "14 January 2023", false);
|
||||||
|
|
||||||
|
// Create a new pause action for the future
|
||||||
|
loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, "20 January 2023", "25 January 2023");
|
||||||
|
|
||||||
|
// Validate Loan Delinquency Pause Period on Loan
|
||||||
|
validateLoanDelinquencyPausePerios(loanId, "10 January 2023", "14 January 2023", false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidationErrorIsThrownWhenCreatingActionInThePast() {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Disburse Loan
|
||||||
|
disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January 2023");
|
||||||
|
|
||||||
|
// Create Delinquency Pause for the Loan in the past
|
||||||
|
CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
|
||||||
|
() -> loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, "05 December 2022", "15 January 2023"));
|
||||||
|
assertTrue(exception.getMessage().contains("Start date of pause period must be in the future"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateLoanDelinquencyPausePerios(Long loanId, String expectedStart, String expectedEnd, Boolean expectedPauseState) {
|
||||||
|
GetLoansLoanIdResponse loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
|
||||||
|
Assertions.assertNotNull(loan.getDelinquent());
|
||||||
|
Assertions.assertEquals(expectedPauseState, loan.getDelinquent().getDelinquencyCalculationPaused());
|
||||||
|
Assertions.assertEquals(LocalDate.parse(expectedStart, dateTimeFormatter),
|
||||||
|
loan.getDelinquent().getDelinquencyPausePeriodStartDate());
|
||||||
|
Assertions.assertEquals(LocalDate.parse(expectedEnd, dateTimeFormatter), loan.getDelinquent().getDelinquencyPausePeriodEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+3
-3
@@ -41,7 +41,7 @@ import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
|
|||||||
import org.apache.fineract.client.models.GetDelinquencyRangesResponse;
|
import org.apache.fineract.client.models.GetDelinquencyRangesResponse;
|
||||||
import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse;
|
import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse;
|
||||||
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
|
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdCollectionData;
|
import org.apache.fineract.client.models.GetLoansLoanIdDelinquencySummary;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
|
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
|
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
@@ -903,7 +903,7 @@ public class DelinquencyBucketsIntegrationTest {
|
|||||||
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
|
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
|
||||||
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
|
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
|
||||||
|
|
||||||
GetLoansLoanIdCollectionData getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
GetLoansLoanIdDelinquencySummary getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||||
assertNotNull(getLoansLoanIdCollectionData);
|
assertNotNull(getLoansLoanIdCollectionData);
|
||||||
assertEquals(0, getLoansLoanIdCollectionData.getDelinquentDays());
|
assertEquals(0, getLoansLoanIdCollectionData.getDelinquentDays());
|
||||||
assertEquals(0, getLoansLoanIdCollectionData.getPastDueDays());
|
assertEquals(0, getLoansLoanIdCollectionData.getPastDueDays());
|
||||||
@@ -992,7 +992,7 @@ public class DelinquencyBucketsIntegrationTest {
|
|||||||
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
|
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
|
||||||
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
|
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
|
||||||
|
|
||||||
GetLoansLoanIdCollectionData getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
GetLoansLoanIdDelinquencySummary getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||||
assertNotNull(getLoansLoanIdCollectionData);
|
assertNotNull(getLoansLoanIdCollectionData);
|
||||||
assertEquals(0, getLoansLoanIdCollectionData.getDelinquentDays());
|
assertEquals(0, getLoansLoanIdCollectionData.getDelinquentDays());
|
||||||
assertEquals(0, getLoansLoanIdCollectionData.getPastDueDays());
|
assertEquals(0, getLoansLoanIdCollectionData.getPastDueDays());
|
||||||
|
|||||||
+32
-2
@@ -45,13 +45,14 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.fineract.client.models.AdvancedPaymentData;
|
import org.apache.fineract.client.models.AdvancedPaymentData;
|
||||||
import org.apache.fineract.client.models.DeleteLoansLoanIdChargesChargeIdResponse;
|
import org.apache.fineract.client.models.DeleteLoansLoanIdChargesChargeIdResponse;
|
||||||
import org.apache.fineract.client.models.DeleteLoansLoanIdResponse;
|
import org.apache.fineract.client.models.DeleteLoansLoanIdResponse;
|
||||||
|
import org.apache.fineract.client.models.GetDelinquencyActionsResponse;
|
||||||
import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse;
|
import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse;
|
||||||
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
|
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
|
||||||
import org.apache.fineract.client.models.GetLoanProductsResponse;
|
import org.apache.fineract.client.models.GetLoanProductsResponse;
|
||||||
import org.apache.fineract.client.models.GetLoansApprovalTemplateResponse;
|
import org.apache.fineract.client.models.GetLoansApprovalTemplateResponse;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdChargesChargeIdResponse;
|
import org.apache.fineract.client.models.GetLoansLoanIdChargesChargeIdResponse;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdChargesTemplateResponse;
|
import org.apache.fineract.client.models.GetLoansLoanIdChargesTemplateResponse;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdCollectionData;
|
import org.apache.fineract.client.models.GetLoansLoanIdDelinquencySummary;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdDisbursementDetails;
|
import org.apache.fineract.client.models.GetLoansLoanIdDisbursementDetails;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
|
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
|
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
|
||||||
@@ -63,6 +64,8 @@ import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionId
|
|||||||
import org.apache.fineract.client.models.GetPaymentTypesResponse;
|
import org.apache.fineract.client.models.GetPaymentTypesResponse;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||||
|
import org.apache.fineract.client.models.PostLoansDelinquencyActionRequest;
|
||||||
|
import org.apache.fineract.client.models.PostLoansDelinquencyActionResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest;
|
import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse;
|
import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse;
|
||||||
import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest;
|
import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest;
|
||||||
@@ -88,6 +91,7 @@ import org.apache.fineract.integrationtests.client.IntegrationTest;
|
|||||||
import org.apache.fineract.integrationtests.common.CommonConstants;
|
import org.apache.fineract.integrationtests.common.CommonConstants;
|
||||||
import org.apache.fineract.integrationtests.common.PaymentTypeHelper;
|
import org.apache.fineract.integrationtests.common.PaymentTypeHelper;
|
||||||
import org.apache.fineract.integrationtests.common.Utils;
|
import org.apache.fineract.integrationtests.common.Utils;
|
||||||
|
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction;
|
||||||
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
|
||||||
import org.apache.poi.ss.usermodel.Workbook;
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
|
||||||
@@ -279,6 +283,32 @@ public class LoanTransactionHelper extends IntegrationTest {
|
|||||||
return GSON.fromJson(response, delinquencyTagsListType);
|
return GSON.fromJson(response, delinquencyTagsListType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<GetDelinquencyActionsResponse> getLoanDelinquencyActions(final Long loanID) {
|
||||||
|
return ok(fineract().loans.getLoanDelinquencyActions(loanID));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<GetDelinquencyActionsResponse> getLoanDelinquencyActions(String externalId) {
|
||||||
|
return ok(fineract().loans.getLoanDelinquencyActions1(externalId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostLoansDelinquencyActionResponse createLoanDelinquencyAction(final Long loanid, DelinquencyAction action, String startDate,
|
||||||
|
String endDate) {
|
||||||
|
PostLoansDelinquencyActionRequest postLoansDelinquencyAction = new PostLoansDelinquencyActionRequest().action(action.name())
|
||||||
|
.startDate(startDate).endDate(endDate).locale("en").dateFormat("dd MMMM yyyy");
|
||||||
|
return ok(fineract().loans.createLoanDelinquencyAction(loanid, postLoansDelinquencyAction));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostLoansDelinquencyActionResponse createLoanDelinquencyAction(String externalId, DelinquencyAction action, String startDate,
|
||||||
|
String endDate) {
|
||||||
|
PostLoansDelinquencyActionRequest postLoansDelinquencyAction = new PostLoansDelinquencyActionRequest().action(action.name())
|
||||||
|
.startDate(startDate).endDate(endDate).locale("en").dateFormat("dd MMMM yyyy");
|
||||||
|
return ok(fineract().loans.createLoanDelinquencyAction1(externalId, postLoansDelinquencyAction));
|
||||||
|
}
|
||||||
|
|
||||||
|
public PostLoansDelinquencyActionResponse createLoanDelinquencyAction(final Long loanid, DelinquencyAction action, String startDate) {
|
||||||
|
return createLoanDelinquencyAction(loanid, action, startDate, null);
|
||||||
|
}
|
||||||
|
|
||||||
public Object getLoanProductDetail(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
|
public Object getLoanProductDetail(final RequestSpecification requestSpec, final ResponseSpecification responseSpec,
|
||||||
final Integer loanProductId, final String jsonAttributeToGetBack) {
|
final Integer loanProductId, final String jsonAttributeToGetBack) {
|
||||||
final String URL = "/fineract-provider/api/v1/loanproducts/" + loanProductId + "?associations=all&" + Utils.TENANT_IDENTIFIER;
|
final String URL = "/fineract-provider/api/v1/loanproducts/" + loanProductId + "?associations=all&" + Utils.TENANT_IDENTIFIER;
|
||||||
@@ -1638,7 +1668,7 @@ public class LoanTransactionHelper extends IntegrationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void printDelinquencyData(GetLoansLoanIdResponse getLoansLoanIdResponse) {
|
public void printDelinquencyData(GetLoansLoanIdResponse getLoansLoanIdResponse) {
|
||||||
GetLoansLoanIdCollectionData getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
GetLoansLoanIdDelinquencySummary getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||||
if (getLoansLoanIdCollectionData != null) {
|
if (getLoansLoanIdCollectionData != null) {
|
||||||
log.info("Loan Delinquency {}", getLoansLoanIdCollectionData.toString());
|
log.info("Loan Delinquency {}", getLoansLoanIdCollectionData.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -33,7 +33,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import org.apache.fineract.client.models.DeleteDelinquencyBucketResponse;
|
import org.apache.fineract.client.models.DeleteDelinquencyBucketResponse;
|
||||||
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
|
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
|
||||||
import org.apache.fineract.client.models.GetDelinquencyRangesResponse;
|
import org.apache.fineract.client.models.GetDelinquencyRangesResponse;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdCollectionData;
|
import org.apache.fineract.client.models.GetLoansLoanIdDelinquencySummary;
|
||||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||||
import org.apache.fineract.client.models.PostDelinquencyBucketResponse;
|
import org.apache.fineract.client.models.PostDelinquencyBucketResponse;
|
||||||
import org.apache.fineract.client.models.PostDelinquencyRangeResponse;
|
import org.apache.fineract.client.models.PostDelinquencyRangeResponse;
|
||||||
@@ -121,7 +121,7 @@ public class DelinquencyBucketsHelper {
|
|||||||
|
|
||||||
public static void evaluateLoanCollectionData(GetLoansLoanIdResponse getLoansLoanIdResponse, Integer pastDueDays,
|
public static void evaluateLoanCollectionData(GetLoansLoanIdResponse getLoansLoanIdResponse, Integer pastDueDays,
|
||||||
Double amountExpected) {
|
Double amountExpected) {
|
||||||
GetLoansLoanIdCollectionData getCollectionData = getLoansLoanIdResponse.getDelinquent();
|
GetLoansLoanIdDelinquencySummary getCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||||
if (getCollectionData != null) {
|
if (getCollectionData != null) {
|
||||||
log.info("Loan Delinquency Data in Days {} and Amount {}", getCollectionData.getPastDueDays(),
|
log.info("Loan Delinquency Data in Days {} and Amount {}", getCollectionData.getPastDueDays(),
|
||||||
getCollectionData.getDelinquentAmount());
|
getCollectionData.getDelinquentAmount());
|
||||||
|
|||||||
Reference in New Issue
Block a user