blob: ceb4475dd502ad0395511eb8ce9aca6e31a41970 [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.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 java.util.Optional;
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.delinquency.helper.DelinquencyEffectivePauseHelper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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 DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper = Mockito.mock(DelinquencyEffectivePauseHelper.class);
private final DelinquencyActionParseAndValidator underTest = new DelinquencyActionParseAndValidator(fromJsonHelper,
delinquencyEffectivePauseHelper);
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);
List<LoanDelinquencyAction> existing = List.of(loanDelinquencyAction(PAUSE, "05 September 2022", "15 September 2022"));
List<LoanDelinquencyActionData> effectiveList = List.of(loanDelinquencyActionData(existing.get(0)));
Mockito.when(delinquencyEffectivePauseHelper.calculateEffectiveDelinquencyList(existing)).thenReturn(effectiveList);
LoanDelinquencyAction parsedDelinquencyAction = underTest.validateAndParseUpdate(command, loan, existing,
localDate("09 September 2022"));
Assertions.assertEquals(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");
List<LoanDelinquencyActionData> effectiveList = List.of(loanDelinquencyActionData(existing.get(0)));
Mockito.when(delinquencyEffectivePauseHelper.calculateEffectiveDelinquencyList(existing)).thenReturn(effectiveList);
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");
List<LoanDelinquencyActionData> effectiveList = List.of(loanDelinquencyActionData(existing.get(0)));
Mockito.when(delinquencyEffectivePauseHelper.calculateEffectiveDelinquencyList(existing)).thenReturn(effectiveList);
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");
List<LoanDelinquencyActionData> effectiveList = List.of(loanDelinquencyActionData(existing.get(0)));
Mockito.when(delinquencyEffectivePauseHelper.calculateEffectiveDelinquencyList(existing)).thenReturn(effectiveList);
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")));
}
@Test
public void testStartDateOrEndDateIsMissingForPause() {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
assertPlatformValidationException("The parameter `startDate` is mandatory",
"loan-delinquency-action-pause-startDate-cannot-be-blank",
() -> underTest.validateAndParseUpdate(delinquencyAction("pause", null, "09 September 2022"), loan, List.of(),
localDate("09 September 2022")));
assertPlatformValidationException("The parameter `endDate` is mandatory", "loan-delinquency-action-pause-endDate-cannot-be-blank",
() -> underTest.validateAndParseUpdate(delinquencyAction("pause", "09 September 2022", null), loan, List.of(),
localDate("09 September 2022")));
}
@Test
public void testStartDateIsMissingForResume() {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
assertPlatformValidationException("The parameter `startDate` is mandatory",
"loan-delinquency-action-resume-startDate-cannot-be-blank", () -> underTest
.validateAndParseUpdate(delinquencyAction("resume", null, null), loan, List.of(), localDate("09 September 2022")));
}
@Test
public void testNewPausePeriodStartingOnExistingEndDate() 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", "18 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 testNewPauseEndingOnExistingStartDate() 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, "20 September 2022", "25 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 testNewPausePeriodStartingOnExistingEffectiveEndDate() 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", "20 September 2022"), //
loanDelinquencyAction(RESUME, "18 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());
}
@NotNull
private JsonCommand delinquencyAction(@Nullable String action, @Nullable String startDate, @Nullable String endDate)
throws JsonProcessingException {
Map<String, Object> map = new HashMap<>();
Optional.ofNullable(action).ifPresent(a -> map.put(ACTION, a));
map.put(DATE_FORMAT, "dd MMMM yyyy");
map.put(LOCALE, "en");
Optional.ofNullable(startDate).ifPresent(sd -> map.put(START_DATE, sd));
Optional.ofNullable(endDate).ifPresent(ed -> map.put(END_DATE, ed));
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 LoanDelinquencyActionData loanDelinquencyActionData(LoanDelinquencyAction loanDelinquencyAction) {
return new LoanDelinquencyActionData(loanDelinquencyAction);
}
private LoanDelinquencyAction loanDelinquencyAction(DelinquencyAction action, String startTime) {
return new LoanDelinquencyAction(null, action, localDate(startTime), null);
}
}