This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 42d419864 FINERACT-1992 Delinquency Pause API Changes
42d419864 is described below
commit 42d41986414e328b657030b7b6b15859bd35d98b
Author: Peter Bagrij <[email protected]>
AuthorDate: Mon Nov 13 18:09:11 2023 +0100
FINERACT-1992 Delinquency Pause API Changes
---
.../commands/service/CommandWrapperBuilder.java | 9 +
.../api/DelinquencyApiResourceSwagger.java | 52 ++++
.../domain/LoanDelinquencyActionRepository.java | 5 +
.../CreateDelinquencyActionCommandHandler.java | 42 +++
.../service/DelinquencyReadPlatformService.java | 3 +
.../DelinquencyReadPlatformServiceImpl.java | 71 +++++
.../service/DelinquencyWritePlatformService.java | 2 +
.../DelinquencyWritePlatformServiceImpl.java | 27 ++
.../starter/DelinquencyConfiguration.java | 14 +-
.../validator/DelinquencyActionParameters.java | 31 +++
.../DelinquencyActionParseAndValidator.java | 199 ++++++++++++++
.../validator/LoanDelinquencyActionData.java | 51 ++++
.../loanaccount/api/LoansApiResource.java | 77 ++++++
.../loanaccount/api/LoansApiResourceSwagger.java | 16 +-
.../portfolio/loanaccount/data/CollectionData.java | 6 +-
.../DelinquencyReadPlatformServiceImplTest.java | 204 ++++++++++++++
.../DelinquencyActionParseAndValidatorTest.java | 293 +++++++++++++++++++++
...cyWritePlatformServiceRangeChangeEventTest.java | 10 +-
.../integrationtests/BaseLoanIntegrationTest.java | 11 +-
.../DelinquencyActionIntegrationTests.java | 259 ++++++++++++++++++
.../DelinquencyBucketsIntegrationTest.java | 6 +-
.../common/loans/LoanTransactionHelper.java | 34 ++-
.../common/products/DelinquencyBucketsHelper.java | 4 +-
23 files changed, 1403 insertions(+), 23 deletions(-)
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 22fcc7237..486abf6c9 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 class CommandWrapperBuilder {
this.href = "/loans/" + loanId + "/transactions?command=downPayment";
return this;
}
+
+ public CommandWrapperBuilder createDelinquencyAction(final Long loanId) {
+ this.actionName = "CREATE";
+ this.entityName = "DELINQUENCY_ACTION";
+ this.entityId = null;
+ this.loanId = loanId;
+ this.href = "/loans/" + loanId + "/delinquency-action";
+ return this;
+ }
}
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 65b8ccdcb..48413f8d1 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 @@ package org.apache.fineract.portfolio.delinquency.api;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDate;
+import java.time.OffsetDateTime;
public final class DelinquencyApiResourceSwagger {
@@ -150,4 +151,55 @@ public final class DelinquencyApiResourceSwagger {
public LocalDate liftedOnDate;
}
+ @Schema(description = "GetDelinquencyActionsResponse")
+ public static final class GetDelinquencyActionsResponse {
+
+ private GetDelinquencyActionsResponse() {}
+
+ @Schema(example = "1")
+ public Long id;
+ @Schema(example = "pause")
+ public String action;
+ @Schema(example = "2013,1,2")
+ public LocalDate startDate;
+ @Schema(example = "2013,2,20")
+ public LocalDate endDate;
+ @Schema(example = "1")
+ public Long createdById;
+ @Schema(example = "1359463135000")
+ public OffsetDateTime createdOn;
+ @Schema(example = "1")
+ public Long updatedById;
+ @Schema(example = "1359463135000")
+ public OffsetDateTime lastModifiedOn;
+ }
+
+ @Schema(description = "PostLoansDelinquencyActionRequest")
+ public static final class PostLoansDelinquencyActionRequest {
+
+ @Schema(example = "pause")
+ public String action;
+ @Schema(example = "2013-01-02")
+ public String startDate;
+ @Schema(example = "2013-02-20")
+ public String endDate;
+ @Schema(example = "yyyy-MM-dd")
+ public String dateFormat;
+ @Schema(example = "en")
+ public String locale;
+ }
+
+ @Schema(description = "PostLoansDelinquencyActionResponse")
+ public static final class PostLoansDelinquencyActionResponse {
+
+ @Schema(example = "1")
+ public Long officeId;
+
+ @Schema(example = "1")
+ public Long clientId;
+
+ @Schema(example = "1")
+ public Long resourceId;
+ }
+
}
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 d8bef639e..8167de590 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<LoanDelinquencyAction>
getEffectiveDelinquencyActionForLoan(@Param("loan_id") Long loan_id,
@Param("business_date") LocalDate business_date, Pageable page);
+
+ List<LoanDelinquencyAction> 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 000000000..3d2160ae1
--- /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 9f3510cce..174cc1283 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.DelinquencyBucketData;
import org.apache.fineract.portfolio.delinquency.data.DelinquencyRangeData;
import
org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
import
org.apache.fineract.portfolio.delinquency.data.LoanInstallmentDelinquencyTagData;
+import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
public interface DelinquencyReadPlatformService {
@@ -43,4 +44,6 @@ public interface DelinquencyReadPlatformService {
Collection<LoanInstallmentDelinquencyTagData>
retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId);
+ Collection<LoanDelinquencyAction> retrieveLoanDelinquencyActions(Long
loanId);
+
}
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 7e1d9c145..4ce2c9f74 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<DelinquencyRangeData> retrieveAllDelinquencyRanges() {
@@ -124,14 +138,71 @@ public class DelinquencyReadPlatformServiceImpl
implements DelinquencyReadPlatfo
collectionData.setLastRepaymentDate(lastRepaymentTransaction.getTransactionDate());
collectionData.setLastRepaymentAmount(lastRepaymentTransaction.getAmount());
}
+
+ enrichWithDelinquencyPausePeriodInfo(collectionData,
retrieveLoanDelinquencyActions(loanId),
+ ThreadLocalContextUtil.getBusinessDate());
}
return collectionData;
}
+ void enrichWithDelinquencyPausePeriodInfo(CollectionData collectionData,
Collection<LoanDelinquencyAction> delinquencyActions,
+ LocalDate businessDate) {
+ // partition them based on type
+ Map<DelinquencyAction, List<LoanDelinquencyAction>> partitioned =
delinquencyActions.stream()
+
.collect(Collectors.groupingBy(LoanDelinquencyAction::getAction));
+
+ // add the possible resumes to it to create the effective pause periods
+ if (partitioned.containsKey(DelinquencyAction.PAUSE)) {
+ List<LoanDelinquencyActionData> effective = new ArrayList<>();
+ List<LoanDelinquencyAction> pauses =
partitioned.get(DelinquencyAction.PAUSE);
+ for (LoanDelinquencyAction loanDelinquencyAction : pauses) {
+ Optional<LoanDelinquencyAction> resume =
findMatchingResume(loanDelinquencyAction, partitioned.get(RESUME));
+ LoanDelinquencyActionData loanDelinquencyActionData = new
LoanDelinquencyActionData(loanDelinquencyAction);
+ resume.ifPresent(r ->
loanDelinquencyActionData.setEndDate(r.getStartDate()));
+ effective.add(loanDelinquencyActionData);
+ }
+
+ // order them by start date, filter out the future items
+ Optional<LoanDelinquencyActionData> last = effective.stream()
+
.sorted(Comparator.comparing(LoanDelinquencyActionData::getStartDate))
+ .filter(e -> !DateUtils.isAfter(e.getStartDate(),
businessDate)) // keep only the present and the
+
// past ones
+ .reduce((first, second) -> second);
+
+ // enrich collectionData
+ last.ifPresent(action -> {
+ collectionData
+
.setDelinquencyCalculationPaused(!action.startDate.isAfter(businessDate) &&
!businessDate.isAfter(action.endDate));
+
collectionData.setDelinquencyPausePeriodStartDate(action.getStartDate());
+
collectionData.setDelinquencyPausePeriodEndDate(action.getEndDate());
+ });
+ }
+ }
+
+ private Optional<LoanDelinquencyAction>
findMatchingResume(LoanDelinquencyAction pause, List<LoanDelinquencyAction>
resumes) {
+ if (resumes != null && resumes.size() > 0) {
+ for (LoanDelinquencyAction resume : resumes) {
+ if (!pause.getStartDate().isAfter(resume.getStartDate()) &&
!resume.getStartDate().isAfter(pause.getEndDate())) {
+ return Optional.of(resume);
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
@Override
public Collection<LoanInstallmentDelinquencyTagData>
retrieveLoanInstallmentsCurrentDelinquencyTag(Long loanId) {
return
repositoryLoanInstallmentDelinquencyTag.findInstallmentDelinquencyTags(loanId);
}
+ @Override
+ public Collection<LoanDelinquencyAction>
retrieveLoanDelinquencyActions(Long loanId) {
+ final Optional<Loan> optLoan = this.loanRepository.findById(loanId);
+ if (optLoan.isPresent()) {
+ return
loanDelinquencyActionRepository.findByLoanOrderById(optLoan.get());
+ }
+ return List.of();
+ }
+
}
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 d5f6a1b06..9db4b45df 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 0520c36d1..af58629fe 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.DelinquencyBucketMapping
import
org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
import org.apache.fineract.portfolio.delinquency.domain.DelinquencyRange;
import
org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
+import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
+import
org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository;
import
org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistory;
import
org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
import
org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTag;
import
org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
import
org.apache.fineract.portfolio.delinquency.exception.DelinquencyBucketAgesOverlapedException;
import
org.apache.fineract.portfolio.delinquency.exception.DelinquencyRangeInvalidAgesException;
+import
org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParseAndValidator;
import
org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
import
org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
import org.apache.fineract.portfolio.loanaccount.data.CollectionData;
@@ -76,6 +79,8 @@ public class DelinquencyWritePlatformServiceImpl implements
DelinquencyWritePlat
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanDelinquencyDomainService loanDelinquencyDomainService;
private final LoanInstallmentDelinquencyTagRepository
loanInstallmentDelinquencyTagRepository;
+ private final LoanDelinquencyActionRepository
loanDelinquencyActionRepository;
+ private final DelinquencyActionParseAndValidator
delinquencyActionParseAndValidator;
@Override
public CommandProcessingResult createDelinquencyRange(JsonCommand command)
{
@@ -199,6 +204,28 @@ public class DelinquencyWritePlatformServiceImpl
implements DelinquencyWritePlat
}
}
+ @Override
+ public CommandProcessingResult createDelinquencyAction(Long loanId,
JsonCommand command) {
+ final Loan loan =
this.loanRepository.findOneWithNotFoundDetection(loanId);
+ final LocalDate businessDate = DateUtils.getBusinessLocalDate();
+ final List<LoanDelinquencyAction> savedDelinquencyList =
loanDelinquencyActionRepository.findByLoanOrderById(loan);
+
+ LoanDelinquencyAction parsedDelinquencyAction =
delinquencyActionParseAndValidator.validateAndParseUpdate(command, loan,
+ savedDelinquencyList, businessDate);
+
+ parsedDelinquencyAction.setLoan(loan);
+
+ LoanDelinquencyAction saved =
loanDelinquencyActionRepository.saveAndFlush(parsedDelinquencyAction);
+
+ return new
CommandProcessingResultBuilder().withCommandId(command.commandId()) //
+ .withEntityId(saved.getId()) //
+ .withOfficeId(loan.getOfficeId()) //
+ .withClientId(loan.getClientId()) //
+ .withGroupId(loan.getGroupId()) //
+ .withLoanId(loanId) //
+ .build();
+ }
+
@Override
public void removeDelinquencyTagToLoan(final Loan loan) {
setLoanDelinquencyTag(loan, null);
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 88c27afa8..1b78a48c2 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.infrastructure.event.business.service.BusinessEventNo
import
org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketMappingsRepository;
import
org.apache.fineract.portfolio.delinquency.domain.DelinquencyBucketRepository;
import
org.apache.fineract.portfolio.delinquency.domain.DelinquencyRangeRepository;
+import
org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyActionRepository;
import
org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyTagHistoryRepository;
import
org.apache.fineract.portfolio.delinquency.domain.LoanInstallmentDelinquencyTagRepository;
import
org.apache.fineract.portfolio.delinquency.mapper.DelinquencyBucketMapper;
@@ -33,6 +34,7 @@ import
org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatfor
import
org.apache.fineract.portfolio.delinquency.service.DelinquencyWritePlatformServiceImpl;
import
org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainService;
import
org.apache.fineract.portfolio.delinquency.service.LoanDelinquencyDomainServiceImpl;
+import
org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParseAndValidator;
import
org.apache.fineract.portfolio.delinquency.validator.DelinquencyBucketParseAndValidator;
import
org.apache.fineract.portfolio.delinquency.validator.DelinquencyRangeParseAndValidator;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
@@ -52,10 +54,11 @@ public class DelinquencyConfiguration {
DelinquencyRangeMapper mapperRange, DelinquencyBucketMapper
mapperBucket,
LoanDelinquencyTagMapper mapperLoanDelinquencyTagHistory,
LoanRepository loanRepository,
LoanDelinquencyDomainService loanDelinquencyDomainService,
- LoanInstallmentDelinquencyTagRepository
repositoryLoanInstallmentDelinquencyTag) {
+ LoanInstallmentDelinquencyTagRepository
repositoryLoanInstallmentDelinquencyTag,
+ LoanDelinquencyActionRepository loanDelinquencyActionRepository) {
return new DelinquencyReadPlatformServiceImpl(repositoryRange,
repositoryBucket, repositoryLoanDelinquencyTagHistory, mapperRange,
mapperBucket, mapperLoanDelinquencyTagHistory, loanRepository,
loanDelinquencyDomainService,
- repositoryLoanInstallmentDelinquencyTag);
+ repositoryLoanInstallmentDelinquencyTag,
loanDelinquencyActionRepository);
}
@Bean
@@ -66,10 +69,13 @@ public class DelinquencyConfiguration {
LoanDelinquencyTagHistoryRepository loanDelinquencyTagRepository,
LoanRepositoryWrapper loanRepository,
LoanProductRepository loanProductRepository,
BusinessEventNotifierService businessEventNotifierService,
LoanDelinquencyDomainService loanDelinquencyDomainService,
- LoanInstallmentDelinquencyTagRepository
loanInstallmentDelinquencyTagRepository) {
+ LoanInstallmentDelinquencyTagRepository
loanInstallmentDelinquencyTagRepository,
+ LoanDelinquencyActionRepository loanDelinquencyActionRepository,
+ DelinquencyActionParseAndValidator
delinquencyActionParseAndValidator) {
return new DelinquencyWritePlatformServiceImpl(dataValidatorBucket,
dataValidatorRange, repositoryRange, repositoryBucket,
repositoryBucketMappings, loanDelinquencyTagRepository,
loanRepository, loanProductRepository, businessEventNotifierService,
- loanDelinquencyDomainService,
loanInstallmentDelinquencyTagRepository);
+ loanDelinquencyDomainService,
loanInstallmentDelinquencyTagRepository, loanDelinquencyActionRepository,
+ delinquencyActionParseAndValidator);
}
@Bean
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 000000000..95d484c5a
--- /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 000000000..eac6670e5
--- /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<LoanDelinquencyAction> savedDelinquencyActions, LocalDate
businessDate) {
+ List<LoanDelinquencyActionData> effectiveDelinquencyList =
calculateEffectiveDelinquencyList(savedDelinquencyActions);
+ LoanDelinquencyAction parsedDelinquencyAction = parseCommand(command);
+ validateLoanIsActive(loan);
+ if
(DelinquencyAction.PAUSE.equals(parsedDelinquencyAction.getAction())) {
+ validatePauseStartAndEndDate(parsedDelinquencyAction,
businessDate);
+ validatePauseShallNotOverlap(parsedDelinquencyAction,
effectiveDelinquencyList);
+ } else if
(DelinquencyAction.RESUME.equals(parsedDelinquencyAction.getAction())) {
+ validateResumeStartDate(parsedDelinquencyAction, businessDate);
+ validateResumeNoEndDate(parsedDelinquencyAction);
+ validateResumeShouldBeOnActivePause(parsedDelinquencyAction,
effectiveDelinquencyList);
+ }
+ return parsedDelinquencyAction;
+ }
+
+ private List<LoanDelinquencyActionData>
calculateEffectiveDelinquencyList(List<LoanDelinquencyAction>
savedDelinquencyActions) {
+ // partition them based on type
+ Map<DelinquencyAction, List<LoanDelinquencyAction>> partitioned =
savedDelinquencyActions.stream()
+
.collect(Collectors.groupingBy(LoanDelinquencyAction::getAction));
+ List<LoanDelinquencyActionData> effective = new ArrayList<>();
+ List<LoanDelinquencyAction> pauses =
partitioned.get(DelinquencyAction.PAUSE);
+ if (pauses != null && pauses.size() > 0) {
+ for (LoanDelinquencyAction loanDelinquencyAction : pauses) {
+ Optional<LoanDelinquencyAction> resume =
findMatchingResume(loanDelinquencyAction, partitioned.get(RESUME));
+ LoanDelinquencyActionData loanDelinquencyActionData = new
LoanDelinquencyActionData(loanDelinquencyAction);
+ resume.ifPresent(r ->
loanDelinquencyActionData.setEndDate(r.getStartDate()));
+ effective.add(loanDelinquencyActionData);
+ }
+ }
+ return effective;
+ }
+
+ private Optional<LoanDelinquencyAction>
findMatchingResume(LoanDelinquencyAction pause, List<LoanDelinquencyAction>
resumes) {
+ if (resumes != null && resumes.size() > 0) {
+ for (LoanDelinquencyAction resume : resumes) {
+ if (!pause.getStartDate().isAfter(resume.getStartDate()) &&
!resume.getStartDate().isAfter(pause.getEndDate())) {
+ return Optional.of(resume);
+ }
+ }
+ }
+ return Optional.empty();
+ }
+
+ private void validateResumeShouldBeOnActivePause(LoanDelinquencyAction
parsedDelinquencyAction,
+ List<LoanDelinquencyActionData> savedDelinquencyActions) {
+ boolean match = savedDelinquencyActions.stream()
+ .anyMatch(lda ->
!DateUtils.isBefore(parsedDelinquencyAction.getStartDate(), lda.getStartDate())
+ &&
!DateUtils.isAfter(parsedDelinquencyAction.getStartDate(), lda.getEndDate()));
+ if (!match) {
+
raiseValidationError("loan-delinquency-action-resume-should-be-on-pause",
+ "Resume Delinquency Action can only be created during an
active pause");
+ }
+ }
+
+ private void validateResumeNoEndDate(LoanDelinquencyAction
parsedDelinquencyAction) {
+ if (parsedDelinquencyAction.getEndDate() != null) {
+
raiseValidationError("loan-delinquency-action-resume-should-have-no-end-date",
+ "Resume Delinquency action can not have end date");
+ }
+ }
+
+ private void validateResumeStartDate(LoanDelinquencyAction
parsedDelinquencyAction, LocalDate businessDate) {
+ if (!parsedDelinquencyAction.getStartDate().equals(businessDate)) {
+ raiseValidationError("loan-delinquency-action-invalid-start-date",
+ "Start date of the Resume Delinquency action must be the
current business date");
+ }
+ }
+
+ private void validatePauseStartAndEndDate(LoanDelinquencyAction
parsedDelinquencyAction, LocalDate businessDate) {
+ if
(parsedDelinquencyAction.getStartDate().equals(parsedDelinquencyAction.getEndDate()))
{
+
raiseValidationError("loan-delinquency-action-invalid-start-date-and-end-date",
+ "Delinquency pause period must be at least one day");
+ }
+
+ if (businessDate.isAfter(parsedDelinquencyAction.getStartDate())) {
+ raiseValidationError("loan-delinquency-action-invalid-start-date",
"Start date of pause period must be in the future");
+ }
+ }
+
+ private void validateLoanIsActive(Loan loan) {
+ if (!loan.getStatus().isActive()) {
+ raiseValidationError("loan-delinquency-action-invalid-loan-state",
"Delinquency actions can be created only for active loans.");
+ }
+ }
+
+ private void validatePauseShallNotOverlap(LoanDelinquencyAction
parsedDelinquencyAction,
+ List<LoanDelinquencyActionData> delinquencyActions) {
+ if (delinquencyActions.stream().filter(lda ->
lda.getAction().equals(DelinquencyAction.PAUSE))
+ .anyMatch(lda -> isOverlapping(parsedDelinquencyAction, lda)))
{
+ raiseValidationError("loan-delinquency-action-overlapping",
+ "Delinquency pause period cannot overlap with another
pause period");
+ }
+ }
+
+ private boolean isOverlapping(LoanDelinquencyAction
parsedDelinquencyAction, LoanDelinquencyActionData ldad) {
+ return
((!parsedDelinquencyAction.getStartDate().isAfter(ldad.getStartDate())
+ &&
!ldad.getStartDate().isAfter(parsedDelinquencyAction.getEndDate()))
+ ||
(!parsedDelinquencyAction.getStartDate().isAfter(ldad.getEndDate())
+ &&
!ldad.getEndDate().isAfter(parsedDelinquencyAction.getEndDate())));
+ }
+
+ @org.jetbrains.annotations.NotNull
+ private LoanDelinquencyAction
parseCommand(@org.jetbrains.annotations.NotNull JsonCommand command) {
+ LoanDelinquencyAction parsedDelinquencyAction = new
LoanDelinquencyAction();
+ parsedDelinquencyAction.setAction(extractAction(command.parsedJson()));
+
parsedDelinquencyAction.setStartDate(extractStartDate(command.parsedJson()));
+
parsedDelinquencyAction.setEndDate(extractEndDate(command.parsedJson()));
+ return parsedDelinquencyAction;
+ }
+
+ private DelinquencyAction extractAction(JsonElement json) {
+ String actionString =
jsonHelper.extractStringNamed(DelinquencyActionParameters.ACTION, json);
+ validateActionString(actionString);
+ if ("pause".equalsIgnoreCase(actionString)) {
+ return DelinquencyAction.PAUSE;
+ } else if ("resume".equalsIgnoreCase(actionString)) {
+ return DelinquencyAction.RESUME;
+ } else {
+ throw new
PlatformApiDataValidationException(List.of(ApiParameterError.generalError("loan-delinquency-action-invalid-action",
+ "Invalid Delinquency Action: " + actionString)));
+ }
+ }
+
+ private void validateActionString(String actionString) {
+ if (StringUtils.isEmpty(actionString)) {
+ raiseValidationError("loan-delinquency-action-missing-action",
"Delinquency Action must not be null or empty");
+ }
+ }
+
+ private LocalDate extractStartDate(JsonElement json) {
+ String dateFormat =
jsonHelper.extractStringNamed(DelinquencyActionParameters.DATE_FORMAT, json);
+ String locale =
jsonHelper.extractStringNamed(DelinquencyActionParameters.LOCALE, json);
+ return
jsonHelper.extractLocalDateNamed(DelinquencyActionParameters.START_DATE, json,
dateFormat,
+ JsonParserHelper.localeFromString(locale));
+ }
+
+ private LocalDate extractEndDate(JsonElement json) {
+ String dateFormat =
jsonHelper.extractStringNamed(DelinquencyActionParameters.DATE_FORMAT, json);
+ String locale =
jsonHelper.extractStringNamed(DelinquencyActionParameters.LOCALE, json);
+ return
jsonHelper.extractLocalDateNamed(DelinquencyActionParameters.END_DATE, json,
dateFormat,
+ JsonParserHelper.localeFromString(locale));
+ }
+
+ private void raiseValidationError(String globalisationMessageCode, String
msg) throws PlatformApiDataValidationException {
+ throw new
PlatformApiDataValidationException(List.of(ApiParameterError.generalError(globalisationMessageCode,
msg)));
+ }
+
+}
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 000000000..8f192165e
--- /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 0cfd44249..e379e841b 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.collateralmanagement.service.LoanCollateral
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import
org.apache.fineract.portfolio.delinquency.api.DelinquencyApiResourceSwagger;
import
org.apache.fineract.portfolio.delinquency.data.LoanDelinquencyTagHistoryData;
+import org.apache.fineract.portfolio.delinquency.domain.LoanDelinquencyAction;
import
org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
+import
org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData;
import org.apache.fineract.portfolio.floatingrates.data.InterestRatePeriodData;
import org.apache.fineract.portfolio.fund.data.FundData;
import org.apache.fineract.portfolio.fund.service.FundReadPlatformService;
@@ -257,6 +259,7 @@ public class LoansApiResource {
private final DefaultToApiJsonSerializer<LoanAccountData>
toApiJsonSerializer;
private final DefaultToApiJsonSerializer<LoanApprovalData>
loanApprovalDataToApiJsonSerializer;
private final DefaultToApiJsonSerializer<LoanScheduleData>
loanScheduleToApiJsonSerializer;
+ private final DefaultToApiJsonSerializer<LoanDelinquencyActionData>
delinquencyActionSerializer;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final FromJsonHelper fromJsonHelper;
private final PortfolioCommandSourceWritePlatformService
commandsSourceWritePlatformService;
@@ -778,6 +781,58 @@ public class LoansApiResource {
return getDelinquencyTagHistory(null, loanExternalId, uriInfo);
}
+ @GET
+ @Path("{loanId}/delinquency-actions")
+ @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Retrieve delinquency actions related to the loan",
description = "")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "OK", content =
@Content(array = @ArraySchema(schema = @Schema(implementation =
DelinquencyApiResourceSwagger.GetDelinquencyActionsResponse.class)))) })
+ public String getLoanDelinquencyActions(@PathParam("loanId")
@Parameter(description = "loanId", required = true) final Long loanId,
+ @Context final UriInfo uriInfo) {
+ return getLoanDelinquencyActions(loanId, null, uriInfo);
+ }
+
+ @GET
+ @Path("external-id/{loanExternalId}/delinquency-actions")
+ @Consumes({ MediaType.TEXT_HTML, MediaType.APPLICATION_JSON })
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Retrieve delinquency actions related to the loan",
description = "")
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "OK", content =
@Content(array = @ArraySchema(schema = @Schema(implementation =
DelinquencyApiResourceSwagger.GetDelinquencyActionsResponse.class)))) })
+ public String getLoanDelinquencyActions(
+ @PathParam("loanExternalId") @Parameter(description =
"loanExternalId", required = true) final String loanExternalId,
+ @Context final UriInfo uriInfo) {
+ return getLoanDelinquencyActions(null, loanExternalId, uriInfo);
+ }
+
+ @POST
+ @Path("{loanId}/delinquency-actions")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Adds a new delinquency action for a loan",
description = "")
+ @RequestBody(required = true, content = @Content(schema =
@Schema(implementation =
DelinquencyApiResourceSwagger.PostLoansDelinquencyActionRequest.class)))
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "OK", content =
@Content(schema = @Schema(implementation =
DelinquencyApiResourceSwagger.PostLoansDelinquencyActionResponse.class))) })
+ public String createLoanDelinquencyAction(@PathParam("loanId")
@Parameter(description = "loanId", required = true) final Long loanId,
+ @Context final UriInfo uriInfo, @Parameter(hidden = true) final
String apiRequestBodyAsJson) {
+ return createLoanDelinquencyAction(loanId, ExternalId.empty(),
apiRequestBodyAsJson);
+ }
+
+ @POST
+ @Path("external-id/{loanExternalId}/delinquency-actions")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ @Operation(summary = "Adds a new delinquency action for a loan",
description = "")
+ @RequestBody(required = true, content = @Content(schema =
@Schema(implementation =
DelinquencyApiResourceSwagger.PostLoansDelinquencyActionRequest.class)))
+ @ApiResponses({
+ @ApiResponse(responseCode = "200", description = "OK", content =
@Content(schema = @Schema(implementation =
DelinquencyApiResourceSwagger.PostLoansDelinquencyActionResponse.class))) })
+ public String createLoanDelinquencyAction(
+ @PathParam("loanExternalId") @Parameter(description =
"loanExternalId", required = true) final String loanExternalId,
+ @Context final UriInfo uriInfo, @Parameter(hidden = true) final
String apiRequestBodyAsJson) {
+ return createLoanDelinquencyAction(null,
ExternalIdFactory.produce(loanExternalId), apiRequestBodyAsJson);
+ }
+
private String retrieveApprovalTemplate(final Long loanId, final String
loanExternalIdStr, final String templateType,
final UriInfo uriInfo) {
this.context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
@@ -1154,4 +1209,26 @@ public class LoansApiResource {
}
return resolvedLoanId;
}
+
+ private String getLoanDelinquencyActions(final Long loanId, final String
loanExternalIdStr, final UriInfo uriInfo) {
+
context.authenticatedUser().validateHasReadPermission(RESOURCE_NAME_FOR_PERMISSIONS);
+ ExternalId loanExternalId =
ExternalIdFactory.produce(loanExternalIdStr);
+ Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId);
+
+ final Collection<LoanDelinquencyAction> delinquencyActions =
this.delinquencyReadPlatformService
+ .retrieveLoanDelinquencyActions(resolvedLoanId);
+ List<LoanDelinquencyActionData> result =
delinquencyActions.stream().map(LoanDelinquencyActionData::new).toList();
+ return this.jsonSerializerTagHistory.serialize(result);
+ }
+
+ private String createLoanDelinquencyAction(Long loanId, ExternalId
loanExternalId, String apiRequestBodyAsJson) {
+
context.authenticatedUser().validateHasUpdatePermission(RESOURCE_NAME_FOR_PERMISSIONS);
+ Long resolvedLoanId = getResolvedLoanId(loanId, loanExternalId);
+
+ CommandWrapperBuilder builder = new
CommandWrapperBuilder().createDelinquencyAction(resolvedLoanId);
+ builder.withJson(apiRequestBodyAsJson);
+ CommandProcessingResult result =
this.commandsSourceWritePlatformService.logCommandSource(builder.build());
+ return delinquencyActionSerializer.serialize(result);
+ }
+
}
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 80a9a8b12..df128b1e1 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 @@ final class LoansApiResourceSwagger {
private String externalId;
}
- static final class GetLoansLoanIdCollectionData {
+ static final class GetLoansLoanIdDelinquencySummary {
- private GetLoansLoanIdCollectionData() {}
+ private GetLoansLoanIdDelinquencySummary() {}
@Schema(example = "100.000000")
public Double availableDisbursementAmount;
@@ -989,6 +989,16 @@ final class LoansApiResourceSwagger {
public LocalDate lastRepaymentDate;
@Schema(example = "100.000000")
public Double lastRepaymentAmount;
+
+ @Schema(example = "true")
+ public Boolean delinquencyCalculationPaused;
+
+ @Schema(example = "[2022, 07, 05]")
+ public LocalDate delinquencyPausePeriodStartDate;
+
+ @Schema(example = "[2022, 07, 10]")
+ public LocalDate delinquencyPausePeriodEndDate;
+
}
@Schema(example = "1")
@@ -1062,7 +1072,7 @@ final class LoansApiResourceSwagger {
@Schema(description = "Set of GetLoansLoanIdDisbursementDetails")
public Set<GetLoansLoanIdDisbursementDetails> disbursementDetails;
@Schema(description = "Delinquent data")
- public GetLoansLoanIdCollectionData delinquent;
+ public GetLoansLoanIdDelinquencySummary delinquent;
@Schema(description = "Set of charges")
public List<GetLoansLoanIdLoanChargeData> charges;
public GetDelinquencyRangesResponse delinquencyRange;
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 75d5cad38..6087f19af 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 000000000..544ec9498
--- /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<LoanDelinquencyAction> delinquencyActions = List.of();
+
+ // when
+ underTest.enrichWithDelinquencyPausePeriodInfo(collectionData,
delinquencyActions, LocalDate.of(2023, JANUARY, 12));
+
+ // then
+
Assertions.assertFalse(collectionData.isDelinquencyCalculationPaused());
+
Assertions.assertNull(collectionData.getDelinquencyPausePeriodStartDate());
+
Assertions.assertNull(collectionData.getDelinquencyPausePeriodEndDate());
+ }
+
+ @Test
+ public void
testMultiplePausesWithoutResumeActionCurrentlyInPauseFirstDay() {
+ // given
+ CollectionData collectionData = CollectionData.template();
+ Collection<LoanDelinquencyAction> delinquencyActions = List.of(
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 10), LocalDate.of(2023, JANUARY, 11)),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 12), LocalDate.of(2023, JANUARY, 13)),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
+
+ // when
+ underTest.enrichWithDelinquencyPausePeriodInfo(collectionData,
delinquencyActions, LocalDate.of(2023, JANUARY, 12));
+
+ // then
+ Assertions.assertTrue(collectionData.isDelinquencyCalculationPaused());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 12),
collectionData.getDelinquencyPausePeriodStartDate());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 13),
collectionData.getDelinquencyPausePeriodEndDate());
+ }
+
+ @Test
+ public void testMultiplePausesWithoutResumeActionCurrentlyInPauseLastDay()
{
+ // given
+ CollectionData collectionData = CollectionData.template();
+ Collection<LoanDelinquencyAction> delinquencyActions = List.of(
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 10), LocalDate.of(2023, JANUARY, 11)),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 12), LocalDate.of(2023, JANUARY, 13)),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
+
+ // when
+ underTest.enrichWithDelinquencyPausePeriodInfo(collectionData,
delinquencyActions, LocalDate.of(2023, JANUARY, 13));
+
+ // then
+ Assertions.assertTrue(collectionData.isDelinquencyCalculationPaused());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 12),
collectionData.getDelinquencyPausePeriodStartDate());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 13),
collectionData.getDelinquencyPausePeriodEndDate());
+ }
+
+ @Test
+ public void
testMultiplePausesWithoutResumeActionCurrentBusinessDateBetweenStartAndEndDate()
{
+ // given
+ CollectionData collectionData = CollectionData.template();
+ Collection<LoanDelinquencyAction> delinquencyActions = List.of(
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 10), LocalDate.of(2023, JANUARY, 11)),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 12), LocalDate.of(2023, JANUARY, 14)),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
+
+ // when
+ underTest.enrichWithDelinquencyPausePeriodInfo(collectionData,
delinquencyActions, LocalDate.of(2023, JANUARY, 13));
+
+ // then
+ Assertions.assertTrue(collectionData.isDelinquencyCalculationPaused());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 12),
collectionData.getDelinquencyPausePeriodStartDate());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 14),
collectionData.getDelinquencyPausePeriodEndDate());
+ }
+
+ @Test
+ public void
testMultiplePausesWithoutResumeCurrentBusinessDateIsNotOverlappingWithAnyOfThePauses()
{
+ // given
+ CollectionData collectionData = CollectionData.template();
+ Collection<LoanDelinquencyAction> delinquencyActions = List.of(
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 10), LocalDate.of(2023, JANUARY, 11)),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 13), LocalDate.of(2023, JANUARY, 14)),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
+
+ // when
+ underTest.enrichWithDelinquencyPausePeriodInfo(collectionData,
delinquencyActions, LocalDate.of(2023, JANUARY, 12));
+
+ // then
+
Assertions.assertFalse(collectionData.isDelinquencyCalculationPaused());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 10),
collectionData.getDelinquencyPausePeriodStartDate());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 11),
collectionData.getDelinquencyPausePeriodEndDate());
+ }
+
+ @Test
+ public void testResumeIsAppliedToOneOfThePauseNotActive() {
+ // given
+ CollectionData collectionData = CollectionData.template();
+ Collection<LoanDelinquencyAction> delinquencyActions = List.of(
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 10), LocalDate.of(2023, JANUARY, 20)),
+ new LoanDelinquencyAction(null, RESUME, LocalDate.of(2023,
JANUARY, 11), null),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 13), LocalDate.of(2023, JANUARY, 14)),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
+
+ // when
+ underTest.enrichWithDelinquencyPausePeriodInfo(collectionData,
delinquencyActions, LocalDate.of(2023, JANUARY, 12));
+
+ // then
+
Assertions.assertFalse(collectionData.isDelinquencyCalculationPaused());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 10),
collectionData.getDelinquencyPausePeriodStartDate());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 11),
collectionData.getDelinquencyPausePeriodEndDate());
+ }
+
+ @Test
+ public void testResumeIsAppliedToOneOfThePauseActive() {
+ // given
+ CollectionData collectionData = CollectionData.template();
+ Collection<LoanDelinquencyAction> delinquencyActions = List.of(
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 10), LocalDate.of(2023, JANUARY, 20)),
+ new LoanDelinquencyAction(null, RESUME, LocalDate.of(2023,
JANUARY, 11), null),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 13), LocalDate.of(2023, JANUARY, 14)),
+ new LoanDelinquencyAction(null, PAUSE, LocalDate.of(2023,
JANUARY, 15), LocalDate.of(2023, JANUARY, 20)));
+
+ // when
+ underTest.enrichWithDelinquencyPausePeriodInfo(collectionData,
delinquencyActions, LocalDate.of(2023, JANUARY, 11));
+
+ // then
+ Assertions.assertTrue(collectionData.isDelinquencyCalculationPaused());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 10),
collectionData.getDelinquencyPausePeriodStartDate());
+ Assertions.assertEquals(LocalDate.of(2023, JANUARY, 11),
collectionData.getDelinquencyPausePeriodEndDate());
+ }
+
+}
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 000000000..daa161095
--- /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<LoanDelinquencyAction> existing =
List.of(loanDelinquencyAction(PAUSE, "14 September 2022", "22 September 2022"));
+ JsonCommand command = delinquencyAction("pause", "09 September 2022",
"15 September 2022");
+
+ assertPlatformValidationException("Delinquency pause period cannot
overlap with another pause period",
+ "loan-delinquency-action-overlapping",
+ () -> underTest.validateAndParseUpdate(command, loan,
existing, localDate("09 September 2022")));
+ }
+
+ @Test
+ public void testPauseStartIsOverlappingWithAnActivePause() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+ List<LoanDelinquencyAction> existing =
List.of(loanDelinquencyAction(PAUSE, "14 September 2022", "22 September 2022"));
+ JsonCommand command = delinquencyAction("pause", "15 September 2022",
"23 September 2022");
+
+ assertPlatformValidationException("Delinquency pause period cannot
overlap with another pause period",
+ "loan-delinquency-action-overlapping",
+ () -> underTest.validateAndParseUpdate(command, loan,
existing, localDate("09 September 2022")));
+ }
+
+ @Test
+ public void testNewPauseEndIsTheSameAsExistingPauseStart() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+ List<LoanDelinquencyAction> existing =
List.of(loanDelinquencyAction(PAUSE, "15 September 2022", "22 September 2022"));
+ JsonCommand command = delinquencyAction("pause", "09 September 2022",
"15 September 2022");
+
+ assertPlatformValidationException("Delinquency pause period cannot
overlap with another pause period",
+ "loan-delinquency-action-overlapping",
+ () -> underTest.validateAndParseUpdate(command, loan,
existing, localDate("09 September 2022")));
+ }
+
+ @Test
+ public void testNewPauseEndIsTheSameAsExistingPauseEnd() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+ List<LoanDelinquencyAction> existing =
List.of(loanDelinquencyAction(PAUSE, "15 September 2022", "22 September 2022"));
+ JsonCommand command = delinquencyAction("pause", "22 September 2022",
"25 September 2022");
+
+ assertPlatformValidationException("Delinquency pause period cannot
overlap with another pause period",
+ "loan-delinquency-action-overlapping",
+ () -> underTest.validateAndParseUpdate(command, loan,
existing, localDate("09 September 2022")));
+ }
+
+ @Test
+ public void testNewPauseEndIsOverlappingWithExistingPause() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+ List<LoanDelinquencyAction> existing =
List.of(loanDelinquencyAction(PAUSE, "15 September 2022", "22 September 2022"));
+ JsonCommand command = delinquencyAction("pause", "13 September 2022",
"20 September 2022");
+
+ assertPlatformValidationException("Delinquency pause period cannot
overlap with another pause period",
+ "loan-delinquency-action-overlapping",
+ () -> underTest.validateAndParseUpdate(command, loan,
existing, localDate("09 September 2022")));
+ }
+
+ @Test
+ public void testNewPauseIsNotOverlappingBecauseThereWasAResume() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+ JsonCommand command = delinquencyAction("pause", "18 September 2022",
"20 September 2022");
+
+ List<LoanDelinquencyAction> existing =
List.of(loanDelinquencyAction(PAUSE, "15 September 2022", "22 September 2022"),
//
+ loanDelinquencyAction(RESUME, "17 September 2022") //
+ );
+
+ LoanDelinquencyAction parsedDelinquencyAction =
underTest.validateAndParseUpdate(command, loan, existing,
+ localDate("18 September 2022"));
+ Assertions.assertEquals(PAUSE, parsedDelinquencyAction.getAction());
+ Assertions.assertEquals(localDate("18 September 2022"),
parsedDelinquencyAction.getStartDate());
+ Assertions.assertEquals(localDate("20 September 2022"),
parsedDelinquencyAction.getEndDate());
+ }
+
+ @Test
+ public void testResumeIsNotOverlappingWithAnActivePause() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+ List<LoanDelinquencyAction> existing =
List.of(loanDelinquencyAction(PAUSE, "05 September 2022", "08 September 2022"));
+ JsonCommand command = delinquencyAction("resume", "09 September 2022",
null);
+
+ assertPlatformValidationException("Resume Delinquency Action can only
be created during an active pause",
+ "loan-delinquency-action-resume-should-be-on-pause",
+ () -> underTest.validateAndParseUpdate(command, loan,
existing, localDate("09 September 2022")));
+ }
+
+ @Test
+ public void testValidationErrorWhenDelinquencyActionIsMissing() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.APPROVED);
+
+ JsonCommand command = delinquencyAction(null, "09 September 2022", "19
September 2022");
+
+ assertPlatformValidationException("Delinquency Action must not be null
or empty", "loan-delinquency-action-missing-action",
+ () -> underTest.validateAndParseUpdate(command, loan,
List.of(), localDate("09 September 2022")));
+ }
+
+ @Test
+ public void testValidationErrorWhenLoanIsNotActive() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.APPROVED);
+
+ JsonCommand command = delinquencyAction("pause", "09 September 2022",
"19 September 2022");
+
+ assertPlatformValidationException("Delinquency actions can be created
only for active loans.",
+ "loan-delinquency-action-invalid-loan-state",
+ () -> underTest.validateAndParseUpdate(command, loan,
List.of(), localDate("09 September 2022")));
+ }
+
+ @Test
+ public void testValidationErrorResumeShouldHaveNoEndDate() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+ JsonCommand command = delinquencyAction("resume", "09 September 2022",
"19 September 2022");
+
+ assertPlatformValidationException("Resume Delinquency action can not
have end date",
+ "loan-delinquency-action-resume-should-have-no-end-date",
+ () -> underTest.validateAndParseUpdate(command, loan,
List.of(), localDate("09 September 2022")));
+ }
+
+ @Test
+ public void testValidationErrorResumeInvalidStartDate() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+ JsonCommand command = delinquencyAction("resume", "09 September 2022",
"19 September 2022");
+
+ assertPlatformValidationException("Start date of the Resume
Delinquency action must be the current business date",
+ "loan-delinquency-action-invalid-start-date",
+ () -> underTest.validateAndParseUpdate(command, loan,
List.of(), localDate("10 September 2022")));
+ }
+
+ @Test
+ public void testValidationErrorPausePeriodShouldBeAtLeastOneDay() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+ JsonCommand command = delinquencyAction("pause", "10 September 2022",
"10 September 2022");
+
+ assertPlatformValidationException("Delinquency pause period must be at
least one day",
+ "loan-delinquency-action-invalid-start-date-and-end-date",
+ () -> underTest.validateAndParseUpdate(command, loan,
List.of(), localDate("09 September 2022")));
+ }
+
+ @Test
+ public void testValidationErrorPausePeriodMustBeInFuture() throws
JsonProcessingException {
+ Loan loan = Mockito.mock(Loan.class);
+ Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
+
+ JsonCommand command = delinquencyAction("pause", "08 September 2022",
"09 September 2022");
+
+ assertPlatformValidationException("Start date of pause period must be
in the future", "loan-delinquency-action-invalid-start-date",
+ () -> underTest.validateAndParseUpdate(command, loan,
List.of(), localDate("09 September 2022")));
+ }
+
+ @NotNull
+ private JsonCommand delinquencyAction(String action, String startDate,
String endDate) throws JsonProcessingException {
+ Map<String, Object> map = new HashMap<>();
+ map.put(ACTION, action);
+ map.put(DATE_FORMAT, "dd MMMM yyyy");
+ map.put(LOCALE, "en");
+ map.put(START_DATE, startDate);
+ map.put(END_DATE, endDate);
+ return createJsonCommand(map);
+ }
+
+ private LocalDate localDate(String date) {
+ return LocalDate.parse(date, DATE_TIME_FORMATTER);
+ }
+
+ @NotNull
+ private JsonCommand createJsonCommand(Map<String, Object> jsonMap) throws
JsonProcessingException {
+ ObjectMapper objectMapper = new ObjectMapper();
+ String json =
objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonMap);
+ return new JsonCommand(null, JsonParser.parseString(json));
+ }
+
+ private void assertPlatformValidationException(String message, String
code, Executable executable) {
+ PlatformApiDataValidationException validationException =
assertThrows(PlatformApiDataValidationException.class, executable);
+ assertPlatformException(message, code, validationException);
+ }
+
+ private void assertPlatformException(String expectedMessage, String
expectedCode,
+ PlatformApiDataValidationException
platformApiDataValidationException) {
+ Assertions.assertEquals(expectedMessage,
platformApiDataValidationException.getErrors().get(0).getDefaultUserMessage());
+ Assertions.assertEquals(expectedCode,
platformApiDataValidationException.getErrors().get(0).getUserMessageGlobalisationCode());
+ }
+
+ private LoanDelinquencyAction loanDelinquencyAction(DelinquencyAction
action, String startTime, String endTime) {
+ return new LoanDelinquencyAction(null, action, localDate(startTime),
localDate(endTime));
+ }
+
+ private LoanDelinquencyAction loanDelinquencyAction(DelinquencyAction
action, String startTime) {
+ return new LoanDelinquencyAction(null, action, localDate(startTime),
null);
+ }
+
+}
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 ab85206e2..d821b5f77 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 class
DelinquencyWritePlatformServiceRangeChangeEventTest {
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new
LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(BigDecimal.ZERO,
2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
- null, null);
+ null, null, false, null, null);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
@@ -239,10 +239,10 @@ public class
DelinquencyWritePlatformServiceRangeChangeEventTest {
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new
LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(BigDecimal.ZERO,
2L, null, 2L, overDueSinceDate, BigDecimal.ZERO, null, null,
- null, null);
+ null, null, false, null, null);
CollectionData installmentCollectionData = new
CollectionData(BigDecimal.ZERO, 2L, null, 2L, overDueSinceDate,
- installmentPrincipalAmount, null, null, null, null);
+ installmentPrincipalAmount, null, null, null, null, false,
null, null);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
installmentsCollection.put(1L, installmentCollectionData);
@@ -305,10 +305,10 @@ public class
DelinquencyWritePlatformServiceRangeChangeEventTest {
LoanScheduleDelinquencyData loanScheduleDelinquencyData = new
LoanScheduleDelinquencyData(1L, overDueSinceDate, 1L,
loanForProcessing);
CollectionData collectionData = new CollectionData(BigDecimal.ZERO,
29L, null, 29L, overDueSinceDate, BigDecimal.ZERO, null, null,
- null, null);
+ null, null, false, null, null);
CollectionData installmentCollectionData = new
CollectionData(BigDecimal.ZERO, 29L, null, 29L, overDueSinceDate,
- installmentPrincipalAmount, null, null, null, null);
+ installmentPrincipalAmount, null, null, null, null, false,
null, null);
Map<Long, CollectionData> installmentsCollection = new HashMap<>();
installmentsCollection.put(1L, installmentCollectionData);
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 5a8bfd240..1c8168cad 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 @@ public abstract class BaseLoanIntegrationTest {
assertNull(loanDetails.getTransactions(), "No transaction is
expected");
} else {
Assertions.assertEquals(transactions.length,
loanDetails.getTransactions().size());
- DateTimeFormatter dateTimeFormatter =
DateTimeFormatter.ofPattern(DATETIME_PATTERN);
Arrays.stream(transactions).forEach(tr -> {
boolean found = loanDetails.getTransactions().stream()
.anyMatch(item -> Objects.equals(item.getAmount(),
tr.amount) && Objects.equals(item.getType().getValue(), tr.type)
@@ -329,8 +329,13 @@ public abstract class BaseLoanIntegrationTest {
protected Long applyAndApproveLoan(Long clientId, Long loanProductId,
String loanDisbursementDate, Double amount,
int numberOfRepayments) {
- PostLoansResponse postLoansResponse = loanTransactionHelper
- .applyLoan(applyLoanRequest(clientId, loanProductId,
loanDisbursementDate, amount, numberOfRepayments));
+ return applyAndApproveLoan(clientId, loanProductId,
loanDisbursementDate, amount, numberOfRepayments, null);
+ }
+
+ protected Long applyAndApproveLoan(Long clientId, Long loanProductId,
String loanDisbursementDate, Double amount,
+ int numberOfRepayments, String externalId) {
+ PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(
+ applyLoanRequest(clientId, loanProductId,
loanDisbursementDate, amount, numberOfRepayments).externalId(externalId));
PostLoansLoanIdResponse approvedLoanResult =
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
approveLoanRequest(amount));
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 000000000..262f106c1
--- /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<GetDelinquencyActionsResponse> loanDelinquencyActions =
loanTransactionHelper.getLoanDelinquencyActions(loanId);
+ Assertions.assertNotNull(loanDelinquencyActions);
+ Assertions.assertEquals(1, loanDelinquencyActions.size());
+ Assertions.assertEquals("PAUSE",
loanDelinquencyActions.get(0).getAction());
+ Assertions.assertEquals(LocalDate.parse("10 January 2023",
dateTimeFormatter), loanDelinquencyActions.get(0).getStartDate());
+ Assertions.assertEquals(LocalDate.parse("15 January 2023",
dateTimeFormatter), loanDelinquencyActions.get(0).getEndDate());
+ });
+ }
+
+ @Test
+ public void testCreateAndReadPauseDelinquencyActionUsingExternalId() {
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
true);
+
+ // Create external ID
+ String externalId = UUID.randomUUID().toString();
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0, 2, externalId);
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Create Delinquency Pause for the Loan
+ PostLoansDelinquencyActionResponse response =
loanTransactionHelper.createLoanDelinquencyAction(externalId, PAUSE,
+ "10 January 2023", "15 January 2023");
+
+ List<GetDelinquencyActionsResponse> loanDelinquencyActions =
loanTransactionHelper.getLoanDelinquencyActions(externalId);
+ Assertions.assertNotNull(loanDelinquencyActions);
+ Assertions.assertEquals(1, loanDelinquencyActions.size());
+ Assertions.assertEquals("PAUSE",
loanDelinquencyActions.get(0).getAction());
+ Assertions.assertEquals(LocalDate.parse("10 January 2023",
dateTimeFormatter), loanDelinquencyActions.get(0).getStartDate());
+ Assertions.assertEquals(LocalDate.parse("15 January 2023",
dateTimeFormatter), loanDelinquencyActions.get(0).getEndDate());
+ });
+ }
+
+ @Test
+ public void testCreatePauseAndResumeDelinquencyAction() {
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0, 2);
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Create Delinquency Pause for the Loan
+ loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE,
"10 January 2023", "15 January 2023");
+
+ // Update business date
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("14 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // Create 2nd Delinquency Resume for the Loan
+ loanTransactionHelper.createLoanDelinquencyAction(loanId, RESUME,
"14 January 2023");
+
+ List<GetDelinquencyActionsResponse> loanDelinquencyActions =
loanTransactionHelper.getLoanDelinquencyActions(loanId);
+ Assertions.assertNotNull(loanDelinquencyActions);
+ Assertions.assertEquals(2, loanDelinquencyActions.size());
+
+ Assertions.assertEquals("PAUSE",
loanDelinquencyActions.get(0).getAction());
+ Assertions.assertEquals(LocalDate.parse("10 January 2023",
dateTimeFormatter), loanDelinquencyActions.get(0).getStartDate());
+ Assertions.assertEquals(LocalDate.parse("15 January 2023",
dateTimeFormatter), loanDelinquencyActions.get(0).getEndDate());
+
+ Assertions.assertEquals("RESUME",
loanDelinquencyActions.get(1).getAction());
+ Assertions.assertEquals(LocalDate.parse("14 January 2023",
dateTimeFormatter), loanDelinquencyActions.get(1).getStartDate());
+ });
+ }
+
+ @Test
+ public void testCreatePauseAndResumeDelinquencyActionWithStatusFlag() {
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0, 2);
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Create Delinquency Pause for the Loan
+ loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE,
"10 January 2023", "15 January 2023");
+
+ // Update business date
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("14 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // Validate Loan Delinquency Pause Period on Loan
+ validateLoanDelinquencyPausePerios(loanId, "10 January 2023", "15
January 2023", true);
+
+ // Create a Resume for the Loan for the current business date, it
is still expected to be in pause
+ loanTransactionHelper.createLoanDelinquencyAction(loanId, RESUME,
"14 January 2023");
+
+ // Validate Loan Delinquency Pause Period on Loan
+ validateLoanDelinquencyPausePerios(loanId, "10 January 2023", "14
January 2023", true);
+
+ // Update business date to 15 January 2023
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("15 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // Validate Loan Delinquency Pause Period on Loan
+ validateLoanDelinquencyPausePerios(loanId, "10 January 2023", "14
January 2023", false);
+
+ // Create a new pause action for the future
+ loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE,
"20 January 2023", "25 January 2023");
+
+ // Validate Loan Delinquency Pause Period on Loan
+ validateLoanDelinquencyPausePerios(loanId, "10 January 2023", "14
January 2023", false);
+ });
+ }
+
+ @Test
+ public void testValidationErrorIsThrownWhenCreatingActionInThePast() {
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0, 2);
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Create Delinquency Pause for the Loan in the past
+ CallFailedRuntimeException exception =
assertThrows(CallFailedRuntimeException.class,
+ () ->
loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, "05 December
2022", "15 January 2023"));
+ assertTrue(exception.getMessage().contains("Start date of pause
period must be in the future"));
+ });
+ }
+
+ private void validateLoanDelinquencyPausePerios(Long loanId, String
expectedStart, String expectedEnd, Boolean expectedPauseState) {
+ GetLoansLoanIdResponse loan =
loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
+ Assertions.assertNotNull(loan.getDelinquent());
+ Assertions.assertEquals(expectedPauseState,
loan.getDelinquent().getDelinquencyCalculationPaused());
+ Assertions.assertEquals(LocalDate.parse(expectedStart,
dateTimeFormatter),
+ loan.getDelinquent().getDelinquencyPausePeriodStartDate());
+ Assertions.assertEquals(LocalDate.parse(expectedEnd,
dateTimeFormatter), loan.getDelinquent().getDelinquencyPausePeriodEndDate());
+ }
+
+ private Long createLoanProductWith25PctDownPayment(boolean
autoDownPaymentEnabled, boolean multiDisburseEnabled) {
+ PostLoanProductsRequest product =
createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct();
+ product.setMultiDisburseLoan(multiDisburseEnabled);
+
+ if (!multiDisburseEnabled) {
+ product.disallowExpectedDisbursements(null);
+ product.setAllowApprovedDisbursedAmountsOverApplied(null);
+ product.overAppliedCalculationType(null);
+ product.overAppliedNumber(null);
+ }
+
+ product.setEnableDownPayment(true);
+
product.setDisbursedAmountPercentageForDownPayment(DOWN_PAYMENT_PERCENTAGE);
+ product.setEnableAutoRepaymentForDownPayment(autoDownPaymentEnabled);
+
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(product);
+ GetLoanProductsProductIdResponse getLoanProductsProductIdResponse =
loanProductHelper
+ .retrieveLoanProductById(loanProductResponse.getResourceId());
+
+ Long loanProductId = loanProductResponse.getResourceId();
+
+ assertEquals(TRUE,
getLoanProductsProductIdResponse.getEnableDownPayment());
+
assertNotNull(getLoanProductsProductIdResponse.getDisbursedAmountPercentageForDownPayment());
+ assertEquals(0,
getLoanProductsProductIdResponse.getDisbursedAmountPercentageForDownPayment().compareTo(DOWN_PAYMENT_PERCENTAGE));
+ assertEquals(autoDownPaymentEnabled,
getLoanProductsProductIdResponse.getEnableAutoRepaymentForDownPayment());
+ assertEquals(multiDisburseEnabled,
getLoanProductsProductIdResponse.getMultiDisburseLoan());
+ return loanProductId;
+ }
+
+}
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 928ea3e60..eb5395d2f 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.GetDelinquencyBucketsResponse;
import org.apache.fineract.client.models.GetDelinquencyRangesResponse;
import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
-import org.apache.fineract.client.models.GetLoansLoanIdCollectionData;
+import org.apache.fineract.client.models.GetLoansLoanIdDelinquencySummary;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
@@ -903,7 +903,7 @@ public class DelinquencyBucketsIntegrationTest {
getLoansLoanIdResponse =
loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
- GetLoansLoanIdCollectionData getLoansLoanIdCollectionData =
getLoansLoanIdResponse.getDelinquent();
+ GetLoansLoanIdDelinquencySummary getLoansLoanIdCollectionData =
getLoansLoanIdResponse.getDelinquent();
assertNotNull(getLoansLoanIdCollectionData);
assertEquals(0, getLoansLoanIdCollectionData.getDelinquentDays());
assertEquals(0, getLoansLoanIdCollectionData.getPastDueDays());
@@ -992,7 +992,7 @@ public class DelinquencyBucketsIntegrationTest {
getLoansLoanIdResponse =
loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId);
loanTransactionHelper.printDelinquencyData(getLoansLoanIdResponse);
- GetLoansLoanIdCollectionData getLoansLoanIdCollectionData =
getLoansLoanIdResponse.getDelinquent();
+ GetLoansLoanIdDelinquencySummary getLoansLoanIdCollectionData =
getLoansLoanIdResponse.getDelinquent();
assertNotNull(getLoansLoanIdCollectionData);
assertEquals(0, getLoansLoanIdCollectionData.getDelinquentDays());
assertEquals(0, getLoansLoanIdCollectionData.getPastDueDays());
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 5b6f78e5f..4794baf70 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 lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.AdvancedPaymentData;
import
org.apache.fineract.client.models.DeleteLoansLoanIdChargesChargeIdResponse;
import org.apache.fineract.client.models.DeleteLoansLoanIdResponse;
+import org.apache.fineract.client.models.GetDelinquencyActionsResponse;
import org.apache.fineract.client.models.GetDelinquencyTagHistoryResponse;
import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
import org.apache.fineract.client.models.GetLoanProductsResponse;
import org.apache.fineract.client.models.GetLoansApprovalTemplateResponse;
import org.apache.fineract.client.models.GetLoansLoanIdChargesChargeIdResponse;
import org.apache.fineract.client.models.GetLoansLoanIdChargesTemplateResponse;
-import org.apache.fineract.client.models.GetLoansLoanIdCollectionData;
+import org.apache.fineract.client.models.GetLoansLoanIdDelinquencySummary;
import org.apache.fineract.client.models.GetLoansLoanIdDisbursementDetails;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
import org.apache.fineract.client.models.GetLoansLoanIdRepaymentSchedule;
@@ -63,6 +64,8 @@ import
org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionId
import org.apache.fineract.client.models.GetPaymentTypesResponse;
import org.apache.fineract.client.models.PostLoanProductsRequest;
import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansDelinquencyActionRequest;
+import org.apache.fineract.client.models.PostLoansDelinquencyActionResponse;
import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest;
import
org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse;
import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest;
@@ -88,6 +91,7 @@ import
org.apache.fineract.integrationtests.client.IntegrationTest;
import org.apache.fineract.integrationtests.common.CommonConstants;
import org.apache.fineract.integrationtests.common.PaymentTypeHelper;
import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Workbook;
@@ -279,6 +283,32 @@ public class LoanTransactionHelper extends IntegrationTest
{
return GSON.fromJson(response, delinquencyTagsListType);
}
+ public List<GetDelinquencyActionsResponse> getLoanDelinquencyActions(final
Long loanID) {
+ return ok(fineract().loans.getLoanDelinquencyActions(loanID));
+ }
+
+ public List<GetDelinquencyActionsResponse>
getLoanDelinquencyActions(String externalId) {
+ return ok(fineract().loans.getLoanDelinquencyActions1(externalId));
+ }
+
+ public PostLoansDelinquencyActionResponse
createLoanDelinquencyAction(final Long loanid, DelinquencyAction action, String
startDate,
+ String endDate) {
+ PostLoansDelinquencyActionRequest postLoansDelinquencyAction = new
PostLoansDelinquencyActionRequest().action(action.name())
+
.startDate(startDate).endDate(endDate).locale("en").dateFormat("dd MMMM yyyy");
+ return ok(fineract().loans.createLoanDelinquencyAction(loanid,
postLoansDelinquencyAction));
+ }
+
+ public PostLoansDelinquencyActionResponse
createLoanDelinquencyAction(String externalId, DelinquencyAction action, String
startDate,
+ String endDate) {
+ PostLoansDelinquencyActionRequest postLoansDelinquencyAction = new
PostLoansDelinquencyActionRequest().action(action.name())
+
.startDate(startDate).endDate(endDate).locale("en").dateFormat("dd MMMM yyyy");
+ return ok(fineract().loans.createLoanDelinquencyAction1(externalId,
postLoansDelinquencyAction));
+ }
+
+ public PostLoansDelinquencyActionResponse
createLoanDelinquencyAction(final Long loanid, DelinquencyAction action, String
startDate) {
+ return createLoanDelinquencyAction(loanid, action, startDate, null);
+ }
+
public Object getLoanProductDetail(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec,
final Integer loanProductId, final String jsonAttributeToGetBack) {
final String URL = "/fineract-provider/api/v1/loanproducts/" +
loanProductId + "?associations=all&" + Utils.TENANT_IDENTIFIER;
@@ -1638,7 +1668,7 @@ public class LoanTransactionHelper extends
IntegrationTest {
}
public void printDelinquencyData(GetLoansLoanIdResponse
getLoansLoanIdResponse) {
- GetLoansLoanIdCollectionData getLoansLoanIdCollectionData =
getLoansLoanIdResponse.getDelinquent();
+ GetLoansLoanIdDelinquencySummary getLoansLoanIdCollectionData =
getLoansLoanIdResponse.getDelinquent();
if (getLoansLoanIdCollectionData != null) {
log.info("Loan Delinquency {}",
getLoansLoanIdCollectionData.toString());
}
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 e034d6c6d..295322beb 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 lombok.extern.slf4j.Slf4j;
import org.apache.fineract.client.models.DeleteDelinquencyBucketResponse;
import org.apache.fineract.client.models.GetDelinquencyBucketsResponse;
import org.apache.fineract.client.models.GetDelinquencyRangesResponse;
-import org.apache.fineract.client.models.GetLoansLoanIdCollectionData;
+import org.apache.fineract.client.models.GetLoansLoanIdDelinquencySummary;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostDelinquencyBucketResponse;
import org.apache.fineract.client.models.PostDelinquencyRangeResponse;
@@ -121,7 +121,7 @@ public class DelinquencyBucketsHelper {
public static void evaluateLoanCollectionData(GetLoansLoanIdResponse
getLoansLoanIdResponse, Integer pastDueDays,
Double amountExpected) {
- GetLoansLoanIdCollectionData getCollectionData =
getLoansLoanIdResponse.getDelinquent();
+ GetLoansLoanIdDelinquencySummary getCollectionData =
getLoansLoanIdResponse.getDelinquent();
if (getCollectionData != null) {
log.info("Loan Delinquency Data in Days {} and Amount {}",
getCollectionData.getPastDueDays(),
getCollectionData.getDelinquentAmount());