FINERACT-1992 Delinquency Pause API Changes

This commit is contained in:
Peter Bagrij
2023-11-14 19:33:57 +01:00
committed by Adam Saghy
parent 71f9f57ba0
commit 42d4198641
23 changed files with 1403 additions and 23 deletions
@@ -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;
}
}
@@ -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);
}
@@ -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);
}
}
@@ -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);
}
@@ -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();
}
}
@@ -47,4 +47,6 @@ public interface DelinquencyWritePlatformService {
void applyDelinquencyTagToLoan(LoanScheduleDelinquencyData loanDelinquencyData);
CommandProcessingResult createDelinquencyAction(Long loanId, JsonCommand command);
}
@@ -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);
@@ -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
@@ -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";
}
@@ -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)));
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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;
@@ -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);
}
}