From 42d41986414e328b657030b7b6b15859bd35d98b Mon Sep 17 00:00:00 2001 From: Peter Bagrij Date: Mon, 13 Nov 2023 18:09:11 +0100 Subject: [PATCH] FINERACT-1992 Delinquency Pause API Changes --- .../service/CommandWrapperBuilder.java | 9 + .../api/DelinquencyApiResourceSwagger.java | 52 ++++ .../LoanDelinquencyActionRepository.java | 5 + ...CreateDelinquencyActionCommandHandler.java | 42 +++ .../DelinquencyReadPlatformService.java | 3 + .../DelinquencyReadPlatformServiceImpl.java | 71 +++++ .../DelinquencyWritePlatformService.java | 2 + .../DelinquencyWritePlatformServiceImpl.java | 27 ++ .../starter/DelinquencyConfiguration.java | 14 +- .../DelinquencyActionParameters.java | 31 ++ .../DelinquencyActionParseAndValidator.java | 199 ++++++++++++ .../validator/LoanDelinquencyActionData.java | 51 +++ .../loanaccount/api/LoansApiResource.java | 77 +++++ .../api/LoansApiResourceSwagger.java | 16 +- .../loanaccount/data/CollectionData.java | 6 +- ...elinquencyReadPlatformServiceImplTest.java | 204 ++++++++++++ ...elinquencyActionParseAndValidatorTest.java | 293 ++++++++++++++++++ ...tePlatformServiceRangeChangeEventTest.java | 10 +- .../BaseLoanIntegrationTest.java | 11 +- .../DelinquencyActionIntegrationTests.java | 259 ++++++++++++++++ .../DelinquencyBucketsIntegrationTest.java | 6 +- .../common/loans/LoanTransactionHelper.java | 34 +- .../products/DelinquencyBucketsHelper.java | 4 +- 23 files changed, 1403 insertions(+), 23 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyActionCommandHandler.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParameters.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParseAndValidator.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/LoanDelinquencyActionData.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParseAndValidatorTest.java create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index 22fcc7237c4..486abf6c9d6 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -3653,4 +3653,13 @@ public CommandWrapperBuilder downPayment(final Long loanId) { 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; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java index 65b8ccdcb7f..48413f8d18f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/api/DelinquencyApiResourceSwagger.java @@ -20,6 +20,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; +import java.time.OffsetDateTime; public final class DelinquencyApiResourceSwagger { @@ -150,4 +151,55 @@ private GetDelinquencyTagHistoryResponse() {} 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; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyActionRepository.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyActionRepository.java index d8bef639eca..8167de5903e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyActionRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/domain/LoanDelinquencyActionRepository.java @@ -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 getEffectiveDelinquencyActionForLoan(@Param("loan_id") Long loan_id, @Param("business_date") LocalDate business_date, Pageable page); + + List findByLoanOrderById(Loan loan); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyActionCommandHandler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyActionCommandHandler.java new file mode 100644 index 00000000000..3d2160ae116 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/handler/CreateDelinquencyActionCommandHandler.java @@ -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); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java index 9f3510cce57..174cc1283ad 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformService.java @@ -23,6 +23,7 @@ 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 retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId); + Collection retrieveLoanDelinquencyActions(Long loanId); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java index 7e1d9c1455e..4ce2c9f74b2 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImpl.java @@ -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 retrieveAllDelinquencyRanges() { @@ -124,14 +138,71 @@ public CollectionData calculateLoanCollectionData(final Long loanId) { collectionData.setLastRepaymentDate(lastRepaymentTransaction.getTransactionDate()); collectionData.setLastRepaymentAmount(lastRepaymentTransaction.getAmount()); } + + enrichWithDelinquencyPausePeriodInfo(collectionData, retrieveLoanDelinquencyActions(loanId), + ThreadLocalContextUtil.getBusinessDate()); } return collectionData; } + void enrichWithDelinquencyPausePeriodInfo(CollectionData collectionData, Collection delinquencyActions, + LocalDate businessDate) { + // partition them based on type + Map> 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 effective = new ArrayList<>(); + List pauses = partitioned.get(DelinquencyAction.PAUSE); + for (LoanDelinquencyAction loanDelinquencyAction : pauses) { + Optional 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 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 findMatchingResume(LoanDelinquencyAction pause, List 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 retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId) { return repositoryLoanInstallmentDelinquencyTag.findInstallmentDelinquencyTags(loanId); } + @Override + public Collection retrieveLoanDelinquencyActions(Long loanId) { + final Optional optLoan = this.loanRepository.findById(loanId); + if (optLoan.isPresent()) { + return loanDelinquencyActionRepository.findByLoanOrderById(optLoan.get()); + } + return List.of(); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java index d5f6a1b060e..9db4b45df9a 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformService.java @@ -47,4 +47,6 @@ public interface DelinquencyWritePlatformService { void applyDelinquencyTagToLoan(LoanScheduleDelinquencyData loanDelinquencyData); + CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand command); + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java index 0520c36d156..af58629fe4c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyWritePlatformServiceImpl.java @@ -44,12 +44,15 @@ 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 void applyDelinquencyTagToLoan(LoanScheduleDelinquencyData loanDelinquenc } } + @Override + public CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand command) { + final Loan loan = this.loanRepository.findOneWithNotFoundDetection(loanId); + final LocalDate businessDate = DateUtils.getBusinessLocalDate(); + final List 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); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java index 88c27afa8c6..1b78a48c267 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/starter/DelinquencyConfiguration.java @@ -22,6 +22,7 @@ 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.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 DelinquencyReadPlatformService delinquencyReadPlatformService(Delinquency 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 DelinquencyWritePlatformService delinquencyWritePlatformService(Delinquen 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 diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParameters.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParameters.java new file mode 100644 index 00000000000..95d484c5a68 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParameters.java @@ -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"; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParseAndValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParseAndValidator.java new file mode 100644 index 00000000000..eac6670e5a8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParseAndValidator.java @@ -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 savedDelinquencyActions, LocalDate businessDate) { + List 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 calculateEffectiveDelinquencyList(List savedDelinquencyActions) { + // partition them based on type + Map> partitioned = savedDelinquencyActions.stream() + .collect(Collectors.groupingBy(LoanDelinquencyAction::getAction)); + List effective = new ArrayList<>(); + List pauses = partitioned.get(DelinquencyAction.PAUSE); + if (pauses != null && pauses.size() > 0) { + for (LoanDelinquencyAction loanDelinquencyAction : pauses) { + Optional 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 findMatchingResume(LoanDelinquencyAction pause, List 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 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 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))); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/LoanDelinquencyActionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/LoanDelinquencyActionData.java new file mode 100644 index 00000000000..8f192165e42 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/delinquency/validator/LoanDelinquencyActionData.java @@ -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(); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index 0cfd44249ef..e379e841b75 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -109,7 +109,9 @@ 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 toApiJsonSerializer; private final DefaultToApiJsonSerializer loanApprovalDataToApiJsonSerializer; private final DefaultToApiJsonSerializer loanScheduleToApiJsonSerializer; + private final DefaultToApiJsonSerializer delinquencyActionSerializer; private final ApiRequestParameterHelper apiRequestParameterHelper; private final FromJsonHelper fromJsonHelper; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; @@ -778,6 +781,58 @@ public String getDelinquencyTagHistory( 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 @@ private Long getResolvedLoanId(final Long loanId, final ExternalId loanExternalI } 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 delinquencyActions = this.delinquencyReadPlatformService + .retrieveLoanDelinquencyActions(resolvedLoanId); + List 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); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index 80a9a8b1233..df128b1e124 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -964,9 +964,9 @@ private GetLoansLoanIdLoanInstallmentChargeData() {} 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 @@ private GetLoansLoanIdCollectionData() {} 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 @@ private GetLoansLoanIdCollectionData() {} @Schema(description = "Set of GetLoansLoanIdDisbursementDetails") public Set disbursementDetails; @Schema(description = "Delinquent data") - public GetLoansLoanIdCollectionData delinquent; + public GetLoansLoanIdDelinquencySummary delinquent; @Schema(description = "Set of charges") public List charges; public GetDelinquencyRangesResponse delinquencyRange; diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java index 75d5cad3879..6087f19afea 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/CollectionData.java @@ -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); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java new file mode 100644 index 00000000000..544ec94983b --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/service/DelinquencyReadPlatformServiceImplTest.java @@ -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 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 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 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 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 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 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 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()); + } + +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParseAndValidatorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParseAndValidatorTest.java new file mode 100644 index 00000000000..daa1610951e --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/delinquency/validator/DelinquencyActionParseAndValidatorTest.java @@ -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 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 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 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 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 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 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 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 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 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); + } + +} diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java index ab85206e2ee..d821b5f7708 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/deliquency/DelinquencyWritePlatformServiceRangeChangeEventTest.java @@ -127,7 +127,7 @@ public void givenLoanAccountWithDelinquencyBucketWhenRangeChangeThenEventIsRaise 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 installmentsCollection = new HashMap<>(); @@ -239,10 +239,10 @@ public void givenLoanAccountWithOverdueInstallmentAndEnableInstallmentThenDelinq 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 installmentsCollection = new HashMap<>(); installmentsCollection.put(1L, installmentCollectionData); @@ -305,10 +305,10 @@ public void givenLoanAccountWithOverdueInstallmentAndEnableInstallmentThenDelinq 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 installmentsCollection = new HashMap<>(); installmentsCollection.put(1L, installmentCollectionData); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 5a8bfd240b0..1c8168cad10 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -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 @@ protected void verifyTransactions(Long loanId, Transaction... transactions) { 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 @@ protected PostLoansLoanIdRequest approveLoanRequest(Double amount) { 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)); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java new file mode 100644 index 00000000000..262f106c10b --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java @@ -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 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 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 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; + } + +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java index 928ea3e601a..eb5395d2fcf 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyBucketsIntegrationTest.java @@ -41,7 +41,7 @@ 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 void testLoanClassificationToValidateNegatives() { 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 void testLoanClassificationUsingAgeingArrears() { 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()); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 5b6f78e5fc9..4794baf703d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -45,13 +45,14 @@ 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.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.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 ArrayList getLoanDelinquencyTags(final return GSON.fromJson(response, delinquencyTagsListType); } + public List getLoanDelinquencyActions(final Long loanID) { + return ok(fineract().loans.getLoanDelinquencyActions(loanID)); + } + + public List 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 void printRepaymentSchedule(GetLoansLoanIdResponse getLoansLoanIdResponse } public void printDelinquencyData(GetLoansLoanIdResponse getLoansLoanIdResponse) { - GetLoansLoanIdCollectionData getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent(); + GetLoansLoanIdDelinquencySummary getLoansLoanIdCollectionData = getLoansLoanIdResponse.getDelinquent(); if (getLoansLoanIdCollectionData != null) { log.info("Loan Delinquency {}", getLoansLoanIdCollectionData.toString()); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyBucketsHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyBucketsHelper.java index e034d6c6d6c..295322beb82 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyBucketsHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/products/DelinquencyBucketsHelper.java @@ -33,7 +33,7 @@ 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 static Integer createDelinquencyBucket(final RequestSpecification request 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());