blob: 163ee27307b2c8231dd793152c516a9156da18a3 [file] [log] [blame]
/**
* 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 static org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParameters.ACTION;
import static org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParameters.END_DATE;
import static org.apache.fineract.portfolio.delinquency.validator.DelinquencyActionParameters.START_DATE;
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())) {
validateBothStartAndEndDatesAreProvided(parsedDelinquencyAction);
validatePauseStartAndEndDate(parsedDelinquencyAction, businessDate);
validatePauseShallNotOverlap(parsedDelinquencyAction, effectiveDelinquencyList);
} else if (DelinquencyAction.RESUME.equals(parsedDelinquencyAction.getAction())) {
validateResumeStartDate(parsedDelinquencyAction, businessDate);
validateResumeNoEndDate(parsedDelinquencyAction);
validateResumeShouldBeOnActivePause(parsedDelinquencyAction, effectiveDelinquencyList);
}
return parsedDelinquencyAction;
}
private void validateBothStartAndEndDatesAreProvided(LoanDelinquencyAction parsedDelinquencyAction) {
if (parsedDelinquencyAction.getStartDate() == null) {
raiseValidationError("loan-delinquency-action-pause-startDate-cannot-be-blank", "The parameter `startDate` is mandatory",
START_DATE);
}
if (parsedDelinquencyAction.getEndDate() == null) {
raiseValidationError("loan-delinquency-action-pause-endDate-cannot-be-blank", "The parameter `endDate` is mandatory", END_DATE);
}
}
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", END_DATE);
}
}
private void validateResumeStartDate(LoanDelinquencyAction parsedDelinquencyAction, LocalDate businessDate) {
if (parsedDelinquencyAction.getStartDate() == null) {
raiseValidationError("loan-delinquency-action-resume-startDate-cannot-be-blank", "The parameter `startDate` is mandatory",
START_DATE);
}
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", START_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",
START_DATE);
}
}
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.parameterError("loan-delinquency-action-invalid-action",
"Invalid Delinquency Action: " + actionString, ACTION)));
}
}
private void validateActionString(String actionString) {
if (StringUtils.isEmpty(actionString)) {
raiseValidationError("loan-delinquency-action-missing-action", "Delinquency Action must not be null or empty", ACTION);
}
}
private LocalDate extractStartDate(JsonElement json) {
String dateFormat = jsonHelper.extractStringNamed(DelinquencyActionParameters.DATE_FORMAT, json);
String locale = jsonHelper.extractStringNamed(DelinquencyActionParameters.LOCALE, json);
return jsonHelper.extractLocalDateNamed(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)));
}
private void raiseValidationError(String globalisationMessageCode, String msg, String fieldName)
throws PlatformApiDataValidationException {
throw new PlatformApiDataValidationException(List.of(ApiParameterError.parameterError(globalisationMessageCode, msg, fieldName)));
}
}