Merge pull request #36 from myrle-krantz/develop

tasks block commands when appropriate.
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
index beea82f..3fbacdd 100644
--- a/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/client/PortfolioManager.java
@@ -287,6 +287,7 @@
           produces = MediaType.APPLICATION_JSON_VALUE,
           consumes = MediaType.APPLICATION_JSON_VALUE
   )
+  @ThrowsException(status = HttpStatus.CONFLICT, exception = TaskOutstanding.class)
   void executeCaseCommand(@PathVariable("productidentifier") final String productIdentifier,
                           @PathVariable("caseidentifier") final String caseIdentifier,
                           @PathVariable("actionidentifier") final String actionIdentifier,
diff --git a/api/src/main/java/io/mifos/portfolio/api/v1/client/TaskOutstanding.java b/api/src/main/java/io/mifos/portfolio/api/v1/client/TaskOutstanding.java
new file mode 100644
index 0000000..fb6c7a7
--- /dev/null
+++ b/api/src/main/java/io/mifos/portfolio/api/v1/client/TaskOutstanding.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2017 The Mifos Initiative.
+ *
+ * Licensed 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 io.mifos.portfolio.api.v1.client;
+
+/**
+ * @author Myrle Krantz
+ */
+public class TaskOutstanding extends RuntimeException {
+}
diff --git a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
index 9e07c3b..437276d 100644
--- a/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
+++ b/component-test/src/main/java/io/mifos/portfolio/AbstractPortfolioTest.java
@@ -27,10 +27,7 @@
 import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
 import io.mifos.portfolio.api.v1.client.PortfolioManager;
 import io.mifos.portfolio.api.v1.domain.*;
-import io.mifos.portfolio.api.v1.events.CaseEvent;
-import io.mifos.portfolio.api.v1.events.ChargeDefinitionEvent;
-import io.mifos.portfolio.api.v1.events.EventConstants;
-import io.mifos.portfolio.api.v1.events.TaskDefinitionEvent;
+import io.mifos.portfolio.api.v1.events.*;
 import io.mifos.portfolio.service.config.PortfolioServiceConfiguration;
 import io.mifos.portfolio.service.internal.util.AccountingAdapter;
 import io.mifos.portfolio.service.internal.util.RhythmAdapter;
@@ -56,10 +53,7 @@
 import javax.validation.Validator;
 import javax.validation.ValidatorFactory;
 import java.math.BigDecimal;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
+import java.util.*;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
@@ -211,10 +205,31 @@
     Assert.assertEquals(nextState.name(), customerCase.getCurrentState());
   }
 
-  boolean individualLoanCommandEventMatches(
-          final IndividualLoanCommandEvent event,
-          final String productIdentifier,
-          final String caseIdentifier)
+  void checkStateTransferFails(final String productIdentifier,
+                                      final String caseIdentifier,
+                                      final Action action,
+                                      final List<AccountAssignment> oneTimeAccountAssignments,
+                                      final String event,
+                                      final Case.State initialState) throws InterruptedException {
+    final Command command = new Command();
+    command.setOneTimeAccountAssignments(oneTimeAccountAssignments);
+    try {
+      portfolioManager.executeCaseCommand(productIdentifier, caseIdentifier, action.name(), command);
+      Assert.fail();
+    }
+    catch (final IllegalArgumentException ignored) {}
+
+    Assert.assertFalse(eventRecorder.waitForMatch(event,
+        (IndividualLoanCommandEvent x) -> individualLoanCommandEventMatches(x, productIdentifier, caseIdentifier)));
+
+    final Case customerCase = portfolioManager.getCase(productIdentifier, caseIdentifier);
+    Assert.assertEquals(customerCase.getCurrentState(), initialState.name());
+  }
+
+  private boolean individualLoanCommandEventMatches(
+      final IndividualLoanCommandEvent event,
+      final String productIdentifier,
+      final String caseIdentifier)
   {
     return event.getProductIdentifier().equals(productIdentifier) &&
             event.getCaseIdentifier().equals(caseIdentifier);
@@ -268,8 +283,8 @@
     ret.setIdentifier(Fixture.generateUniqueIdentifer("task"));
     ret.setDescription("But how do you feel about this?");
     ret.setName("feep");
-    ret.setMandatory(false);
-    ret.setActions(new HashSet<>());
+    ret.setMandatory(true);
+    ret.setActions(Collections.singleton(Action.APPROVE.name()));
     ret.setFourEyes(false);
     return ret;
   }
@@ -279,4 +294,11 @@
     Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_PRODUCT_ENABLE, product.getIdentifier()));
   }
 
+  void markTaskExecuted(final Product product,
+                        final Case customerCase,
+                        final TaskDefinition taskDefinition) throws InterruptedException {
+    portfolioManager.markTaskExecution(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier(), true);
+    Assert.assertTrue(eventRecorder.wait(EventConstants.PUT_TASK_INSTANCE_EXECUTION, new TaskInstanceEvent(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier())));
+  }
+
 }
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
index 4d7a7b4..08bad1c 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestAccountingInteractionInLoanWorkflow.java
@@ -26,10 +26,7 @@
 import io.mifos.individuallending.api.v1.domain.workflow.Action;
 import io.mifos.individuallending.api.v1.events.IndividualLoanCommandEvent;
 import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
-import io.mifos.portfolio.api.v1.domain.Case;
-import io.mifos.portfolio.api.v1.domain.ChargeDefinition;
-import io.mifos.portfolio.api.v1.domain.CostComponent;
-import io.mifos.portfolio.api.v1.domain.Product;
+import io.mifos.portfolio.api.v1.domain.*;
 import io.mifos.portfolio.api.v1.events.ChargeDefinitionEvent;
 import io.mifos.portfolio.api.v1.events.EventConstants;
 import io.mifos.rhythm.spi.v1.client.BeatListener;
@@ -60,6 +57,7 @@
 
   private Product product = null;
   private Case customerCase = null;
+  private TaskDefinition taskDefinition = null;
   private CaseParameters caseParameters = null;
   private String pendingDisbursalAccountIdentifier = null;
   private String customerLoanAccountIdentifier = null;
@@ -107,6 +105,8 @@
     Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_CHARGE_DEFINITION,
         new ChargeDefinitionEvent(product.getIdentifier(), interestChargeDefinition.getIdentifier())));
 
+    taskDefinition = createTaskDefinition(product);
+
     portfolioManager.enableProduct(product.getIdentifier(), true);
     Assert.assertTrue(this.eventRecorder.wait(EventConstants.PUT_PRODUCT_ENABLE, product.getIdentifier()));
   }
@@ -162,6 +162,9 @@
   //Approve the case, accept a loan origination fee, and prepare to disburse the loan by earmarking the funds.
   private void step4ApproveCase() throws InterruptedException {
     logger.info("step4ApproveCase");
+
+    markTaskExecuted(product, customerCase, taskDefinition);
+
     checkStateTransfer(
         product.getIdentifier(),
         customerCase.getIdentifier(),
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
index 5e47c01..c13daf4 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestCommands.java
@@ -198,25 +198,4 @@
         DISBURSE_INDIVIDUALLOAN_CASE,
         Case.State.PENDING);
   }
-
-  public void checkStateTransferFails(final String productIdentifier,
-                                      final String caseIdentifier,
-                                      final Action action,
-                                      final List<AccountAssignment> oneTimeAccountAssignments,
-                                      final String event,
-                                      final Case.State initialState) throws InterruptedException {
-    final Command command = new Command();
-    command.setOneTimeAccountAssignments(oneTimeAccountAssignments);
-    try {
-      portfolioManager.executeCaseCommand(productIdentifier, caseIdentifier, action.name(), command);
-      Assert.fail();
-    }
-    catch (final IllegalArgumentException ignored) {}
-
-    Assert.assertFalse(eventRecorder.waitForMatch(event,
-            (IndividualLoanCommandEvent x) -> individualLoanCommandEventMatches(x, productIdentifier, caseIdentifier)));
-
-    final Case customerCase = portfolioManager.getCase(productIdentifier, caseIdentifier);
-    Assert.assertEquals(customerCase.getCurrentState(), initialState.name());
-  }
 }
\ No newline at end of file
diff --git a/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java b/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java
index f5e446c..0fef315 100644
--- a/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java
+++ b/component-test/src/main/java/io/mifos/portfolio/TestTaskInstances.java
@@ -18,7 +18,10 @@
 import io.mifos.core.api.context.AutoUserContext;
 import io.mifos.core.api.util.NotFoundException;
 import io.mifos.core.test.domain.TimeStampChecker;
+import io.mifos.individuallending.api.v1.domain.workflow.Action;
+import io.mifos.individuallending.api.v1.events.IndividualLoanEventConstants;
 import io.mifos.portfolio.api.v1.client.TaskExecutionBySameUserAsCaseCreation;
+import io.mifos.portfolio.api.v1.client.TaskOutstanding;
 import io.mifos.portfolio.api.v1.domain.Case;
 import io.mifos.portfolio.api.v1.domain.Product;
 import io.mifos.portfolio.api.v1.domain.TaskDefinition;
@@ -29,6 +32,9 @@
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 
 /**
@@ -82,8 +88,7 @@
     final TaskInstance taskInstance = portfolioManager.getTaskForCase(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier());
 
     final TimeStampChecker timeStampChecker = TimeStampChecker.roughlyNow();
-    portfolioManager.markTaskExecution(product.getIdentifier(), customerCase.getIdentifier(),  taskDefinition.getIdentifier(), true);
-    Assert.assertTrue(eventRecorder.wait(EventConstants.PUT_TASK_INSTANCE_EXECUTION, new TaskInstanceEvent(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier())));
+    markTaskExecuted(product, customerCase, taskDefinition);
 
     final TaskInstance executedTaskInstance = portfolioManager.getTaskForCase(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier());
     timeStampChecker.assertCorrect(executedTaskInstance.getExecutedOn());
@@ -105,8 +110,7 @@
 
     final Case customerCase = createCase(product.getIdentifier());
 
-    portfolioManager.markTaskExecution(product.getIdentifier(), customerCase.getIdentifier(),  taskDefinition.getIdentifier(), true);
-    Assert.assertTrue(eventRecorder.wait(EventConstants.PUT_TASK_INSTANCE_EXECUTION, new TaskInstanceEvent(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier())));
+    markTaskExecuted(product, customerCase, taskDefinition);
 
     final TaskInstance executedTaskInstance = portfolioManager.getTaskForCase(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier());
     Assert.assertNotNull(executedTaskInstance.getExecutedBy());
@@ -152,7 +156,6 @@
     portfolioManager.markTaskExecution(product.getIdentifier(), customerCase.getIdentifier(),  taskDefinition.getIdentifier(), true);
   }
 
-
   @Test
   public void fourEyesCanBeMarkedByDifferentUser() throws InterruptedException {
     final Product product = createProduct();
@@ -167,7 +170,42 @@
     final Case customerCase = createCase(product.getIdentifier());
 
     try (final AutoUserContext ignored = this.tenantApplicationSecurityEnvironment.createAutoUserContext("fred")) {
-      portfolioManager.markTaskExecution(product.getIdentifier(), customerCase.getIdentifier(), taskDefinition.getIdentifier(), true);
+      markTaskExecuted(product, customerCase, taskDefinition);
     }
   }
+
+  @Test
+  public void caseCannotBeOpenedUntilTaskCompleted() throws InterruptedException {
+    final Product product = createProduct();
+
+    final TaskDefinition taskDefinition = createTaskDefinition(product);
+    taskDefinition.setActions(new HashSet<>(Arrays.asList(Action.OPEN.name(), Action.APPROVE.name())));
+    portfolioManager.changeTaskDefinition(product.getIdentifier(), taskDefinition.getIdentifier(), taskDefinition);
+    eventRecorder.wait(EventConstants.PUT_TASK_DEFINITION, new TaskDefinitionEvent(product.getIdentifier(), taskDefinition.getIdentifier()));
+
+    enableProduct(product);
+
+    final Case customerCase = createCase(product.getIdentifier());
+
+    try {
+      checkStateTransferFails(
+          product.getIdentifier(),
+          customerCase.getIdentifier(),
+          Action.OPEN,
+          Collections.singletonList(assignEntryToTeller()),
+          IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE,
+          Case.State.CREATED);
+    }
+    catch (final TaskOutstanding ignored) {}
+
+    markTaskExecuted(product, customerCase, taskDefinition);
+
+    checkStateTransfer(
+        product.getIdentifier(),
+        customerCase.getIdentifier(),
+        Action.OPEN,
+        Collections.singletonList(assignEntryToTeller()),
+        IndividualLoanEventConstants.OPEN_INDIVIDUALLOAN_CASE,
+        Case.State.PENDING);
+  }
 }
diff --git a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
index 549dc9b..4e8fe11 100644
--- a/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
+++ b/service/src/main/java/io/mifos/individuallending/internal/command/handler/IndividualLoanCommandHandler.java
@@ -35,8 +35,7 @@
 import io.mifos.portfolio.api.v1.domain.CostComponent;
 import io.mifos.portfolio.api.v1.events.EventConstants;
 import io.mifos.portfolio.service.internal.mapper.CaseMapper;
-import io.mifos.portfolio.service.internal.repository.CaseEntity;
-import io.mifos.portfolio.service.internal.repository.CaseRepository;
+import io.mifos.portfolio.service.internal.repository.*;
 import io.mifos.portfolio.service.internal.util.AccountingAdapter;
 import io.mifos.portfolio.service.internal.util.ChargeInstance;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -63,15 +62,18 @@
   private final CaseRepository caseRepository;
   private final CostComponentService costComponentService;
   private final AccountingAdapter accountingAdapter;
+  private final TaskInstanceRepository taskInstanceRepository;
 
   @Autowired
   public IndividualLoanCommandHandler(
       final CaseRepository caseRepository,
       final CostComponentService costComponentService,
-      final AccountingAdapter accountingAdapter) {
+      final AccountingAdapter accountingAdapter,
+      final TaskInstanceRepository taskInstanceRepository) {
     this.caseRepository = caseRepository;
     this.costComponentService = costComponentService;
     this.accountingAdapter = accountingAdapter;
+    this.taskInstanceRepository = taskInstanceRepository;
   }
 
   @Transactional
@@ -86,6 +88,8 @@
             productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
     IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.OPEN);
 
+    checkIfTasksAreOutstanding(dataContextOfAction, Action.OPEN);
+
     final CostComponentsForRepaymentPeriod costComponents
         = costComponentService.getCostComponentsForOpen(dataContextOfAction);
 
@@ -124,6 +128,8 @@
         productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
     IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.DENY);
 
+    checkIfTasksAreOutstanding(dataContextOfAction, Action.DENY);
+
     final CostComponentsForRepaymentPeriod costComponents
         = costComponentService.getCostComponentsForDeny(dataContextOfAction);
 
@@ -156,7 +162,7 @@
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
     IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.APPROVE);
 
-    //TODO: Check for incomplete task instances.
+    checkIfTasksAreOutstanding(dataContextOfAction, Action.APPROVE);
 
     final DesignatorToAccountIdentifierMapper designatorToAccountIdentifierMapper
             = new DesignatorToAccountIdentifierMapper(dataContextOfAction);
@@ -206,6 +212,7 @@
         productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
     IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.DISBURSE);
 
+    checkIfTasksAreOutstanding(dataContextOfAction, Action.DISBURSE);
 
     final CostComponentsForRepaymentPeriod costComponentsForRepaymentPeriod =
         costComponentService.getCostComponentsForDisburse(dataContextOfAction);
@@ -253,6 +260,7 @@
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
         productIdentifier, caseIdentifier, null);
     IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.APPLY_INTEREST);
+
     if (dataContextOfAction.getCustomerCase().getEndOfTerm() == null)
       throw ServiceException.internalError(
           "End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
@@ -290,6 +298,9 @@
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(
         productIdentifier, caseIdentifier, null);
     IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.ACCEPT_PAYMENT);
+
+    checkIfTasksAreOutstanding(dataContextOfAction, Action.ACCEPT_PAYMENT);
+
     if (dataContextOfAction.getCustomerCase().getEndOfTerm() == null)
       throw ServiceException.internalError(
           "End of term not set for active case ''{0}.{1}.''", productIdentifier, caseIdentifier);
@@ -343,6 +354,9 @@
     final String caseIdentifier = command.getCaseIdentifier();
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
     IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.WRITE_OFF);
+
+    checkIfTasksAreOutstanding(dataContextOfAction, Action.WRITE_OFF);
+
     final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
     customerCase.setCurrentState(Case.State.CLOSED.name());
     caseRepository.save(customerCase);
@@ -357,6 +371,9 @@
     final String caseIdentifier = command.getCaseIdentifier();
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
     IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.CLOSE);
+
+    checkIfTasksAreOutstanding(dataContextOfAction, Action.CLOSE);
+
     final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
     customerCase.setCurrentState(Case.State.CLOSED.name());
     caseRepository.save(customerCase);
@@ -371,6 +388,9 @@
     final String caseIdentifier = command.getCaseIdentifier();
     final DataContextOfAction dataContextOfAction = costComponentService.checkedGetDataContext(productIdentifier, caseIdentifier, command.getCommand().getOneTimeAccountAssignments());
     IndividualLendingPatternFactory.checkActionCanBeExecuted(Case.State.valueOf(dataContextOfAction.getCustomerCase().getCurrentState()), Action.RECOVER);
+
+    checkIfTasksAreOutstanding(dataContextOfAction, Action.RECOVER);
+
     final CaseEntity customerCase = dataContextOfAction.getCustomerCase();
     customerCase.setCurrentState(Case.State.CLOSED.name());
     caseRepository.save(customerCase);
@@ -434,4 +454,14 @@
                   CostComponent::getAmount,
                   BigDecimal::add)));
   }
+
+  private void checkIfTasksAreOutstanding(final DataContextOfAction dataContextOfAction, final Action action) {
+    final String productIdentifier = dataContextOfAction.getProduct().getIdentifier();
+    final String caseIdentifier = dataContextOfAction.getCustomerCase().getIdentifier();
+    final boolean tasksOutstanding = taskInstanceRepository.areTasksOutstanding(
+        productIdentifier, caseIdentifier, action.name());
+    if (tasksOutstanding)
+      throw ServiceException.conflict("Cannot execute action ''{0}'' for case ''{1}.{2}'' because tasks are incomplete.",
+          action.name(), productIdentifier, caseIdentifier);
+  }
 }
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/repository/TaskInstanceRepository.java b/service/src/main/java/io/mifos/portfolio/service/internal/repository/TaskInstanceRepository.java
index a6cf051..75ad882 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/repository/TaskInstanceRepository.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/repository/TaskInstanceRepository.java
@@ -39,4 +39,13 @@
   @SuppressWarnings("JpaQlInspection")
   @Query("SELECT t FROM TaskInstanceEntity t WHERE t.taskDefinition.product.identifier = :productIdentifier AND t.customerCase.identifier = :caseIdentifier AND t.taskDefinition.identifier = :taskIdentifier")
   Optional<TaskInstanceEntity> findByProductIdAndCaseIdAndTaskId(@Param("productIdentifier") String productId, @Param("caseIdentifier") String caseId, @Param("taskIdentifier") String taskId);
+
+  default boolean areTasksOutstanding(final String productIdentifier, final String caseIdentifier, final String action) {
+    return this.findByProductIdAndCaseId(
+        productIdentifier, caseIdentifier)
+        .filter(taskInstance -> taskInstance.getExecutedOn() == null)
+        .map(TaskInstanceEntity::getTaskDefinition)
+        .filter(TaskDefinitionEntity::getMandatory)
+        .anyMatch(taskDefinition -> taskDefinition.getActions().contains(action));
+  }
 }
diff --git a/service/src/main/java/io/mifos/portfolio/service/internal/service/TaskInstanceService.java b/service/src/main/java/io/mifos/portfolio/service/internal/service/TaskInstanceService.java
index 37cfff1..9395ef8 100644
--- a/service/src/main/java/io/mifos/portfolio/service/internal/service/TaskInstanceService.java
+++ b/service/src/main/java/io/mifos/portfolio/service/internal/service/TaskInstanceService.java
@@ -59,4 +59,10 @@
     return taskInstanceRepository.findByProductIdAndCaseIdAndTaskId(productIdentifier, caseIdentifier, taskIdentifier)
         .map(TaskInstanceMapper::map);
   }
+
+  public boolean areTasksOutstanding(final String productIdentifier,
+                                     final String caseIdentifier,
+                                     final String actionIdentifier) {
+    return taskInstanceRepository.areTasksOutstanding(productIdentifier, caseIdentifier, actionIdentifier);
+  }
 }
\ No newline at end of file
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
index 8b0eeba..fb4750f 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/CaseRestController.java
@@ -29,6 +29,7 @@
 import io.mifos.portfolio.service.internal.command.CreateCaseCommand;
 import io.mifos.portfolio.service.internal.service.CaseService;
 import io.mifos.portfolio.service.internal.service.ProductService;
+import io.mifos.portfolio.service.internal.service.TaskInstanceService;
 import io.mifos.products.spi.ProductCommandDispatcher;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
@@ -52,15 +53,18 @@
   private final CommandGateway commandGateway;
   private final CaseService caseService;
   private final ProductService productService;
+  private final TaskInstanceService taskInstanceService;
 
   @Autowired public CaseRestController(
-          final CommandGateway commandGateway,
-          final CaseService caseService,
-          final ProductService productService) {
+      final CommandGateway commandGateway,
+      final CaseService caseService,
+      final ProductService productService,
+      final TaskInstanceService taskInstanceService) {
     super();
     this.commandGateway = commandGateway;
     this.caseService = caseService;
     this.productService = productService;
+    this.taskInstanceService = taskInstanceService;
   }
 
   @Permittable(value = AcceptedTokenType.TENANT, groupId = PermittableGroupIds.CASE_MANAGEMENT)
@@ -93,9 +97,8 @@
             .ifPresent(x -> {throw ServiceException.conflict("Duplicate identifier: " + productIdentifier + "." + x.getIdentifier());});
 
     final Optional<Boolean> productEnabled = productService.findEnabledByIdentifier(productIdentifier);
-    productEnabled.orElseThrow(() -> ServiceException.internalError("Product should exist, but doesn't"));
-    productEnabled.ifPresent(x -> {
-      if (!x) throw ServiceException.badRequest("Product must be enabled before cases for it can be created: " + productIdentifier);});
+    if (!productEnabled.orElseThrow(() -> ServiceException.internalError("Product should exist, but doesn't"))) {
+      throw ServiceException.badRequest("Product must be enabled before cases for it can be created: " + productIdentifier);}
 
     if (!instance.getProductIdentifier().equals(productIdentifier))
       throw ServiceException.badRequest("Product identifier in request body must match product identifier in request path.");
@@ -210,6 +213,13 @@
     if (!nextActions.contains(actionIdentifier))
       throw ServiceException.badRequest("Action " + actionIdentifier + " cannot be taken from current state.");
 
+
+    final boolean tasksOutstanding = taskInstanceService.areTasksOutstanding(
+        productIdentifier, caseIdentifier, actionIdentifier);
+    if (tasksOutstanding)
+      throw ServiceException.conflict("Cannot execute action ''{0}'' for case ''{1}.{2}'' because tasks are incomplete.",
+          actionIdentifier, productIdentifier, caseIdentifier);
+
     final ProductCommandDispatcher productCommandDispatcher = caseService.getProductCommandDispatcher(productIdentifier);
     productCommandDispatcher.dispatch(productIdentifier, caseIdentifier, actionIdentifier, command);
 
diff --git a/service/src/main/java/io/mifos/portfolio/service/rest/TaskInstanceRestController.java b/service/src/main/java/io/mifos/portfolio/service/rest/TaskInstanceRestController.java
index 001d60a..ea53c0f 100644
--- a/service/src/main/java/io/mifos/portfolio/service/rest/TaskInstanceRestController.java
+++ b/service/src/main/java/io/mifos/portfolio/service/rest/TaskInstanceRestController.java
@@ -72,7 +72,7 @@
   public @ResponseBody
   List<TaskInstance> getAllTasksForCase(@PathVariable("productidentifier") final String productIdentifier,
                                         @PathVariable("caseidentifier") final String caseIdentifier,
-                                        @RequestParam(value = "includeExecuted", required = false) final Boolean includeExecuted)
+                                        @RequestParam(value = "includeExecuted", required = false, defaultValue = "true") final Boolean includeExecuted)
   {
     checkedGetCase(productIdentifier, caseIdentifier);