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