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());