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";
|
||||
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 java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
|
||||
public final class DelinquencyApiResourceSwagger {
|
||||
|
||||
@@ -150,4 +151,55 @@ public final class DelinquencyApiResourceSwagger {
|
||||
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;
|
||||
|
||||
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.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
@@ -35,4 +37,7 @@ public interface LoanDelinquencyActionRepository
|
||||
+ " da.loan.id = :loan_id order by da.createdDate desc")
|
||||
Page<LoanDelinquencyAction> getEffectiveDelinquencyActionForLoan(@Param("loan_id") Long loan_id,
|
||||
@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.LoanDelinquencyTagHistoryData;
|
||||
import org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
|
||||
import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
||||
|
||||
public interface DelinquencyReadPlatformService {
|
||||
@@ -43,4 +44,6 @@ public interface DelinquencyReadPlatformService {
|
||||
|
||||
Collection<LoanInstallmentDelinquencyTagData> retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId);
|
||||
|
||||
Collection<LoanDelinquencyAction> retrieveLoanDelinquencyActions(Long loanId);
|
||||
|
||||
}
|
||||
|
||||
+71
@@ -18,24 +18,37 @@
|
||||
*/
|
||||
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.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
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.DelinquencyRangeData;
|
||||
import org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
|
||||
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.DelinquencyBucketRepository;
|
||||
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
|
||||
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.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.delinquency.validator.LoanDelinquencyActionData;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||
@@ -55,6 +68,7 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
||||
private final LoanRepository loanRepository;
|
||||
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
|
||||
private final LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag;
|
||||
private final LoanDelinquencyActionRepository loanDelinquencyActionRepository;
|
||||
|
||||
@Override
|
||||
public Collection<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
|
||||
@@ -124,14 +138,71 @@ public class DelinquencyReadPlatformServiceImpl implements DelinquencyReadPlatfo
|
||||
collectionData.setLastRepaymentDate(lastRepaymentTransaction.getTransactionDate());
|
||||
collectionData.setLastRepaymentAmount(lastRepaymentTransaction.getAmount());
|
||||
}
|
||||
|
||||
enrichWithDelinquencyPausePeriodInfo(collectionData, retrieveLoanDelinquencyActions(loanId),
|
||||
ThreadLocalContextUtil.getBusinessDate());
|
||||
}
|
||||
|
||||
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
|
||||
public Collection<LoanInstallmentDelinquencyTagData> retrieveLoanInstallmentsCurrentDelinquencyTag(Long 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);
|
||||
|
||||
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.DelinquencyRange;
|
||||
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.LoanDelinquencyTagHistoryRepository;
|
||||
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag;
|
||||
import org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
|
||||
import org.apache.fineract.portfolio.delinquency.exception.DelinquencyBucketAgesOverlapedException;
|
||||
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.DelinquencyRangeParseAndValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
|
||||
@@ -76,6 +79,8 @@ public class DelinquencyWritePlatformServiceImpl implements DelinquencyWritePlat
|
||||
private final BusinessEventNotifierService businessEventNotifierService;
|
||||
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
|
||||
private final LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository;
|
||||
private final LoanDelinquencyActionRepository loanDelinquencyActionRepository;
|
||||
private final DelinquencyActionParseAndValidator delinquencyActionParseAndValidator;
|
||||
|
||||
@Override
|
||||
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
|
||||
public void removeDelinquencyTagToLoan(final Loan loan) {
|
||||
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.DelinquencyBucketRepository;
|
||||
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.LoanInstallmentDelinquencyTagRepository;
|
||||
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.LoanDelinquencyDomainService;
|
||||
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.DelinquencyRangeParseAndValidator;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
|
||||
@@ -52,10 +54,11 @@ public class DelinquencyConfiguration {
|
||||
DelinquencyRangeMapper mapperRange, DelinquencyBucketMapper mapperBucket,
|
||||
LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory, LoanRepository loanRepository,
|
||||
LoanDelinquencyDomainService loanDelinquencyDomainService,
|
||||
LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag) {
|
||||
LoanInstallmentDelinquencyTagRepository repositoryLoanInstallmentDelinquencyTag,
|
||||
LoanDelinquencyActionRepository loanDelinquencyActionRepository) {
|
||||
return new DelinquencyReadPlatformServiceImpl(repositoryRange, repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
|
||||
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository, loanDelinquencyDomainService,
|
||||
repositoryLoanInstallmentDelinquencyTag);
|
||||
repositoryLoanInstallmentDelinquencyTag, loanDelinquencyActionRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -66,10 +69,13 @@ public class DelinquencyConfiguration {
|
||||
LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository, LoanRepositoryWrapper loanRepository,
|
||||
LoanProductRepository loanProductRepository, BusinessEventNotifierService businessEventNotifierService,
|
||||
LoanDelinquencyDomainService loanDelinquencyDomainService,
|
||||
LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository) {
|
||||
LoanInstallmentDelinquencyTagRepository loanInstallmentDelinquencyTagRepository,
|
||||
LoanDelinquencyActionRepository loanDelinquencyActionRepository,
|
||||
DelinquencyActionParseAndValidator delinquencyActionParseAndValidator) {
|
||||
return new DelinquencyWritePlatformServiceImpl(dataValidatorBucket, dataValidatorRange, repositoryRange, repositoryBucket,
|
||||
repositoryBucketMappings, loanDelinquencyTagRepository, loanRepository, loanProductRepository, businessEventNotifierService,
|
||||
loanDelinquencyDomainService, loanInstallmentDelinquencyTagRepository);
|
||||
loanDelinquencyDomainService, loanInstallmentDelinquencyTagRepository, loanDelinquencyActionRepository,
|
||||
delinquencyActionParseAndValidator);
|
||||
}
|
||||
|
||||
@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.delinquency.api.DelinquencyApiResourceSwagger;
|
||||
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.validator.LoanDelinquencyActionData;
|
||||
import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData;
|
||||
import org.apache.fineract.portfolio.fund.data.FundData;
|
||||
import org.apache.fineract.portfolio.fund.service.FundReadPlatformService;
|
||||
@@ -257,6 +259,7 @@ public class LoansApiResource {
|
||||
private final DefaultToApiJsonSerializer<LoanAccountData> toApiJsonSerializer;
|
||||
private final DefaultToApiJsonSerializer<LoanApprovalData> loanApprovalDataToApiJsonSerializer;
|
||||
private final DefaultToApiJsonSerializer<LoanScheduleData> loanScheduleToApiJsonSerializer;
|
||||
private final DefaultToApiJsonSerializer<LoanDelinquencyActionData> delinquencyActionSerializer;
|
||||
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||
private final FromJsonHelper fromJsonHelper;
|
||||
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;
|
||||
@@ -778,6 +781,58 @@ public class LoansApiResource {
|
||||
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,
|
||||
final UriInfo uriInfo) {
|
||||
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
|
||||
@@ -1154,4 +1209,26 @@ public class LoansApiResource {
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
static final class GetLoansLoanIdCollectionData {
|
||||
static final class GetLoansLoanIdDelinquencySummary {
|
||||
|
||||
private GetLoansLoanIdCollectionData() {}
|
||||
private GetLoansLoanIdDelinquencySummary() {}
|
||||
|
||||
@Schema(example = "100.000000")
|
||||
public Double availableDisbursementAmount;
|
||||
@@ -989,6 +989,16 @@ final class LoansApiResourceSwagger {
|
||||
public LocalDate lastRepaymentDate;
|
||||
@Schema(example = "100.000000")
|
||||
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")
|
||||
@@ -1062,7 +1072,7 @@ final class LoansApiResourceSwagger {
|
||||
@Schema(description = "Set of GetLoansLoanIdDisbursementDetails")
|
||||
public Set<GetLoansLoanIdDisbursementDetails> disbursementDetails;
|
||||
@Schema(description = "Delinquent data")
|
||||
public GetLoansLoanIdCollectionData delinquent;
|
||||
public GetLoansLoanIdDelinquencySummary delinquent;
|
||||
@Schema(description = "Set of charges")
|
||||
public List<GetLoansLoanIdLoanChargeData> charges;
|
||||
public GetDelinquencyRangesResponse delinquencyRange;
|
||||
|
||||
+5
-1
@@ -43,9 +43,13 @@ public final class CollectionData {
|
||||
private LocalDate lastRepaymentDate;
|
||||
private BigDecimal lastRepaymentAmount;
|
||||
|
||||
public boolean delinquencyCalculationPaused;
|
||||
public LocalDate delinquencyPausePeriodStartDate;
|
||||
public LocalDate delinquencyPausePeriodEndDate;
|
||||
|
||||
public static CollectionData template() {
|
||||
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,
|
||||
loanForProcessing);
|
||||
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<>();
|
||||
|
||||
@@ -239,10 +239,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||
loanForProcessing);
|
||||
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,
|
||||
installmentPrincipalAmount, null, null, null, null);
|
||||
installmentPrincipalAmount, null, null, null, null, false, null, null);
|
||||
|
||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||
installmentsCollection.put(1L, installmentCollectionData);
|
||||
@@ -305,10 +305,10 @@ public class DelinquencyWritePlatformServiceRangeChangeEventTest {
|
||||
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
|
||||
loanForProcessing);
|
||||
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,
|
||||
installmentPrincipalAmount, null, null, null, null);
|
||||
installmentPrincipalAmount, null, null, null, null, false, null, null);
|
||||
|
||||
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
|
||||
installmentsCollection.put(1L, installmentCollectionData);
|
||||
|
||||
+8
-3
@@ -79,6 +79,7 @@ public abstract class BaseLoanIntegrationTest {
|
||||
protected final LoanProductHelper loanProductHelper = new LoanProductHelper();
|
||||
protected JournalEntryHelper journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec);
|
||||
protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
|
||||
protected DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
|
||||
|
||||
// asset
|
||||
protected final Account loansReceivableAccount = accountHelper.createAssetAccount();
|
||||
@@ -229,7 +230,6 @@ public abstract class BaseLoanIntegrationTest {
|
||||
assertNull(loanDetails.getTransactions(), "No transaction is expected");
|
||||
} else {
|
||||
Assertions.assertEquals(transactions.length, loanDetails.getTransactions().size());
|
||||
DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN);
|
||||
Arrays.stream(transactions).forEach(tr -> {
|
||||
boolean found = loanDetails.getTransactions().stream()
|
||||
.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,
|
||||
int numberOfRepayments) {
|
||||
PostLoansResponse postLoansResponse = loanTransactionHelper
|
||||
.applyLoan(applyLoanRequest(clientId, loanProductId, loanDisbursementDate, amount, numberOfRepayments));
|
||||
return applyAndApproveLoan(clientId, loanProductId, loanDisbursementDate, amount, numberOfRepayments, null);
|
||||
}
|
||||
|
||||
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(),
|
||||
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.GetDelinquencyTagHistoryResponse;
|
||||
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.GetLoansLoanIdRepaymentSchedule;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
@@ -903,7 +903,7 @@ public class DelinquencyBucketsIntegrationTest {
|
||||
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
|
||||
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
|
||||
|
||||
GetLoansLoanIdCollectionData getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||
GetLoansLoanIdDelinquencySummary getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||
assertNotNull(getLoansLoanIdCollectionData);
|
||||
assertEquals(0, getLoansLoanIdCollectionData.getDelinquentDays());
|
||||
assertEquals(0, getLoansLoanIdCollectionData.getPastDueDays());
|
||||
@@ -992,7 +992,7 @@ public class DelinquencyBucketsIntegrationTest {
|
||||
getLoansLoanIdResponse = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
|
||||
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
|
||||
|
||||
GetLoansLoanIdCollectionData getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||
GetLoansLoanIdDelinquencySummary getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||
assertNotNull(getLoansLoanIdCollectionData);
|
||||
assertEquals(0, getLoansLoanIdCollectionData.getDelinquentDays());
|
||||
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.DeleteLoansLoanIdChargesChargeIdResponse;
|
||||
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.GetLoanProductsProductIdResponse;
|
||||
import org.apache.fineract.client.models.GetLoanProductsResponse;
|
||||
import org.apache.fineract.client.models.GetLoansApprovalTemplateResponse;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdChargesChargeIdResponse;
|
||||
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.GetLoansLoanIdRepaymentPeriod;
|
||||
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.PostLoanProductsRequest;
|
||||
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.PostLoansLoanIdChargesChargeIdResponse;
|
||||
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.PaymentTypeHelper;
|
||||
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.ss.usermodel.Workbook;
|
||||
|
||||
@@ -279,6 +283,32 @@ public class LoanTransactionHelper extends IntegrationTest {
|
||||
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,
|
||||
final Integer loanProductId, final String jsonAttributeToGetBack) {
|
||||
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) {
|
||||
GetLoansLoanIdCollectionData getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||
GetLoansLoanIdDelinquencySummary getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||
if (getLoansLoanIdCollectionData != null) {
|
||||
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.GetDelinquencyBucketsResponse;
|
||||
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.PostDelinquencyBucketResponse;
|
||||
import org.apache.fineract.client.models.PostDelinquencyRangeResponse;
|
||||
@@ -121,7 +121,7 @@ public class DelinquencyBucketsHelper {
|
||||
|
||||
public static void evaluateLoanCollectionData(GetLoansLoanIdResponse getLoansLoanIdResponse, Integer pastDueDays,
|
||||
Double amountExpected) {
|
||||
GetLoansLoanIdCollectionData getCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||
GetLoansLoanIdDelinquencySummary getCollectionData = getLoansLoanIdResponse.getDelinquent();
|
||||
if (getCollectionData != null) {
|
||||
log.info("Loan Delinquency Data in Days {} and Amount {}", getCollectionData.getPastDueDays(),
|
||||
getCollectionData.getDelinquentAmount());
|
||||
|
||||
Reference in New Issue
Block a user