FINERACT-1971: Delinquency pause validation bug fixes
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
index 41f8763..380785c 100644
--- 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
@@ -61,6 +61,7 @@
} else if (DelinquencyAction.RESUME.equals(parsedDelinquencyAction.getAction())) {
validateResumeStartDate(parsedDelinquencyAction, businessDate);
validateResumeNoEndDate(parsedDelinquencyAction);
+ validateResumeDoesNotExist(parsedDelinquencyAction, savedDelinquencyActions);
validateResumeShouldBeOnActivePause(parsedDelinquencyAction, effectiveDelinquencyList);
}
return parsedDelinquencyAction;
@@ -87,6 +88,17 @@
}
}
+ private void validateResumeDoesNotExist(LoanDelinquencyAction parsedDelinquencyAction,
+ List<LoanDelinquencyAction> savedDelinquencyActions) {
+ boolean match = savedDelinquencyActions.stream() //
+ .filter(action -> DelinquencyAction.RESUME.equals(action.getAction())) //
+ .anyMatch(action -> parsedDelinquencyAction.getStartDate().isEqual(action.getStartDate()));
+ if (match) {
+ raiseValidationError("loan-delinquency-action-resume-should-be-unique",
+ "There is an existing Resume Delinquency Action on this date");
+ }
+ }
+
private void validateResumeNoEndDate(LoanDelinquencyAction parsedDelinquencyAction) {
if (parsedDelinquencyAction.getEndDate() != null) {
raiseValidationError("loan-delinquency-action-resume-should-have-no-end-date",
@@ -163,7 +175,8 @@
*/
private boolean isOverlapping(LoanDelinquencyAction parsed, LoanDelinquencyActionData existing) {
return (parsed.getEndDate().isAfter(existing.getStartDate()) && parsed.getEndDate().isBefore(existing.getEndDate()))
- || (parsed.getStartDate().isAfter(existing.getStartDate()) && parsed.getStartDate().isBefore(existing.getEndDate()));
+ || (parsed.getStartDate().isAfter(existing.getStartDate()) && parsed.getStartDate().isBefore(existing.getEndDate()))
+ || (parsed.getStartDate().isEqual(existing.getStartDate()) && parsed.getEndDate().isEqual(existing.getEndDate()));
}
@org.jetbrains.annotations.NotNull
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
index ceb4475..2c1d363 100644
--- 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
@@ -36,6 +36,7 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
@@ -137,6 +138,21 @@
}
@Test
+ public void testNewPauseIsOverlappingWithExistingPauseBecauseSameDates() 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", "15 September 2022", "22 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);
@@ -215,6 +231,32 @@
}
@Test
+ public void testValidationErrorResumeOnExistingResumeDate() 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());
+
+ List<LoanDelinquencyAction> existing2 = List.of(loanDelinquencyAction(PAUSE, "05 September 2022", "15 September 2022"),
+ loanDelinquencyAction(RESUME, "09 September 2022", null));
+
+ JsonCommand command2 = delinquencyAction("resume", "09 September 2022", null);
+
+ assertPlatformValidationException("There is an existing Resume Delinquency Action on this date",
+ "loan-delinquency-action-resume-should-be-unique",
+ () -> underTest.validateAndParseUpdate(command2, loan, existing2, localDate("09 September 2022")));
+ }
+
+ @Test
public void testValidationErrorPausePeriodShouldBeAtLeastOneDay() throws JsonProcessingException {
Loan loan = Mockito.mock(Loan.class);
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
@@ -348,7 +390,7 @@
}
private LoanDelinquencyAction loanDelinquencyAction(DelinquencyAction action, String startTime, String endTime) {
- return new LoanDelinquencyAction(null, action, localDate(startTime), localDate(endTime));
+ return new LoanDelinquencyAction(null, action, localDate(startTime), Objects.isNull(endTime) ? null : localDate(endTime));
}
private LoanDelinquencyActionData loanDelinquencyActionData(LoanDelinquencyAction loanDelinquencyAction) {
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
index 2f95ef3..ac409ad 100644
--- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java
+++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DelinquencyActionIntegrationTests.java
@@ -222,6 +222,31 @@
});
}
+ @Test
+ public void testValidationErrorIsThrownWhenCreatingActionThatOverlaps() {
+ 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, "01 January 2023", "15 January 2023");
+
+ // Create overlapping Delinquency Pause for the Loan
+ CallFailedRuntimeException exception = assertThrows(CallFailedRuntimeException.class,
+ () -> loanTransactionHelper.createLoanDelinquencyAction(loanId, PAUSE, "01 January 2023", "15 January 2023"));
+ assertTrue(exception.getMessage().contains("Delinquency pause period cannot overlap with another pause period"));
+ });
+ }
+
private void validateLoanDelinquencyPausePeriods(Long loanId, GetLoansLoanIdDelinquencyPausePeriod... pausePeriods) {
GetLoansLoanIdResponse loan = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
Assertions.assertNotNull(loan.getDelinquent());