content: /*
   * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
   *
   * 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 org.activiti.engine.test.bpmn.subprocess.transaction;
  
  import static org.assertj.core.api.Assertions.assertThat;
  import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
  
  import java.util.List;
  import org.activiti.engine.ActivitiException;
  import org.activiti.engine.impl.EventSubscriptionQueryImpl;
  import org.activiti.engine.impl.history.HistoryLevel;
  import org.activiti.engine.impl.persistence.entity.EventSubscriptionEntity;
  import org.activiti.engine.impl.test.PluggableActivitiTestCase;
  import org.activiti.engine.runtime.Execution;
  import org.activiti.engine.runtime.ProcessInstance;
  import org.activiti.engine.task.Task;
  import org.activiti.engine.test.Deployment;
  
  /**
  
   */
  public class TransactionSubProcessTest extends PluggableActivitiTestCase {
  
      @Deployment(
          resources = {
              "org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testSimpleCase.bpmn20.xml",
          }
      )
      public void testSimpleCaseTxSuccessful() {
          ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess");
  
          // after the process is started, we have compensate event subscriptions:
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("undoBookHotel").count()
          ).isEqualTo(1);
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()
          ).isEqualTo(1);
  
          // the task is present:
          Task task = taskService.createTaskQuery().singleResult();
          assertThat(task).isNotNull();
  
          // making the tx succeed:
          taskService.setVariable(task.getId(), "confirmed", true);
          taskService.complete(task.getId());
  
          // now the process instance execution is sitting in the 'afterSuccess' task
          // -> has left the transaction using the "normal" sequence flow
          List<String> activeActivityIds = runtimeService.getActiveActivityIds(processInstance.getId());
          assertThat(activeActivityIds.contains("afterSuccess")).isTrue();
  
          // there is a compensate event subscription for the transaction under the process instance
          EventSubscriptionEntity eventSubscriptionEntity = createEventSubscriptionQuery()
              .eventType("compensate")
              .activityId("tx")
              .executionId(processInstance.getId())
              .singleResult();
  
          // there is an event-scope execution associated with the event-subscription:
          assertThat(eventSubscriptionEntity.getConfiguration()).isNotNull();
          Execution eventScopeExecution = runtimeService
              .createExecutionQuery()
              .executionId(eventSubscriptionEntity.getConfiguration())
              .singleResult();
          assertThat(eventScopeExecution).isNotNull();
  
          // we still have compensate event subscriptions for the compensation handlers, only now they are part of the event scope
          assertThat(
              createEventSubscriptionQuery()
                  .eventType("compensate")
                  .activityId("undoBookHotel")
                  .executionId(eventScopeExecution.getId())
                  .count()
          ).isEqualTo(1);
          assertThat(
              createEventSubscriptionQuery()
                  .eventType("compensate")
                  .activityId("undoBookFlight")
                  .executionId(eventScopeExecution.getId())
                  .count()
          ).isEqualTo(1);
          assertThat(
              createEventSubscriptionQuery()
                  .eventType("compensate")
                  .activityId("undoChargeCard")
                  .executionId(eventScopeExecution.getId())
                  .count()
          ).isEqualTo(1);
  
          // assert that the compensation handlers have not been invoked:
          assertThat(runtimeService.getVariable(processInstance.getId(), "undoBookHotel")).isNull();
          assertThat(runtimeService.getVariable(processInstance.getId(), "undoBookFlight")).isNull();
          assertThat(runtimeService.getVariable(processInstance.getId(), "undoChargeCard")).isNull();
  
          // end the process instance
          Execution receiveExecution = runtimeService.createExecutionQuery().activityId("afterSuccess").singleResult();
          runtimeService.trigger(receiveExecution.getId());
          assertProcessEnded(processInstance.getId());
          assertThat(runtimeService.createExecutionQuery().count()).isEqualTo(0);
      }
  
      @Deployment(
          resources = {
              "org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testSimpleCase.bpmn20.xml",
          }
      )
      public void testSimpleCaseTxCancelled() {
          ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess");
  
          // after the process is started, we have compensate event subscriptions:
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("undoBookHotel").count()
          ).isEqualTo(1);
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()
          ).isEqualTo(1);
  
          // the task is present:
          Task task = taskService.createTaskQuery().singleResult();
          assertThat(task).isNotNull();
          assertThat(task.getTaskDefinitionKey()).isEqualTo("askCustomer");
  
          // making the tx fail:
          taskService.setVariable(task.getId(), "confirmed", false);
          taskService.complete(task.getId());
  
          // now the process instance execution is sitting in the 'afterCancellation' task
          // -> has left the transaction using the cancel boundary event
          Execution execution = runtimeService
              .createExecutionQuery()
              .processInstanceId(processInstance.getId())
              .activityId("afterCancellation")
              .singleResult();
          assertThat(execution).isNotNull();
  
          // we have no more compensate event subscriptions
          assertThat(createEventSubscriptionQuery().eventType("compensate").count()).isEqualTo(0);
  
          // assert that the compensation handlers have been invoked:
          assertThat(runtimeService.getVariable(processInstance.getId(), "undoBookHotel")).isEqualTo(1);
          assertThat(runtimeService.getVariable(processInstance.getId(), "undoBookFlight")).isEqualTo(1);
          assertThat(runtimeService.getVariable(processInstance.getId(), "undoChargeCard")).isEqualTo(1);
  
          // if we have history, we check that the invocation of the compensation
          // handlers is recorded in history.
          if (processEngineConfiguration.getHistoryLevel().isAtLeast(HistoryLevel.ACTIVITY)) {
              assertThat(
                  historyService.createHistoricActivityInstanceQuery().activityId("undoBookFlight").count()
              ).isEqualTo(1);
              assertThat(
                  historyService.createHistoricActivityInstanceQuery().activityId("undoBookHotel").count()
              ).isEqualTo(1);
              assertThat(
                  historyService.createHistoricActivityInstanceQuery().activityId("undoChargeCard").count()
              ).isEqualTo(1);
          }
  
          // end the process instance
          Execution receiveExecution = runtimeService
              .createExecutionQuery()
              .activityId("afterCancellation")
              .singleResult();
          runtimeService.trigger(receiveExecution.getId());
          assertProcessEnded(processInstance.getId());
          assertThat(runtimeService.createExecutionQuery().count()).isEqualTo(0);
      }
  
      @Deployment
      public void testCancelEndConcurrent() {
          ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess");
  
          // after the process is started, we have compensate event subscriptions:
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("undoBookHotel").count()
          ).isEqualTo(1);
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()
          ).isEqualTo(1);
  
          // the task is present:
          Task task = taskService.createTaskQuery().singleResult();
          assertThat(task).isNotNull();
          assertThat(task.getTaskDefinitionKey()).isEqualTo("askCustomer");
  
          // making the tx fail:
          taskService.setVariable(task.getId(), "confirmed", false);
          taskService.complete(task.getId());
  
          // now the process instance execution is sitting in the 'afterCancellation' task
          // -> has left the transaction using the cancel boundary event
          List<String> activeActivityIds = runtimeService.getActiveActivityIds(processInstance.getId());
          assertThat(activeActivityIds.contains("afterCancellation")).isTrue();
  
          // we have no more compensate event subscriptions
          assertThat(createEventSubscriptionQuery().eventType("compensate").count()).isEqualTo(0);
  
          // assert that the compensation handlers have been invoked:
          assertThat(runtimeService.getVariable(processInstance.getId(), "undoBookHotel")).isEqualTo(1);
          assertThat(runtimeService.getVariable(processInstance.getId(), "undoBookFlight")).isEqualTo(1);
  
          // if we have history, we check that the invocation of the compensation handlers is recorded in history.
          if (processEngineConfiguration.getHistoryLevel().isAtLeast(HistoryLevel.ACTIVITY)) {
              assertThat(
                  historyService.createHistoricActivityInstanceQuery().activityId("undoBookHotel").count()
              ).isEqualTo(1);
              assertThat(
                  historyService.createHistoricActivityInstanceQuery().activityId("undoBookFlight").count()
              ).isEqualTo(1);
          }
  
          // end the process instance
          Execution receiveExecution = runtimeService
              .createExecutionQuery()
              .activityId("afterCancellation")
              .singleResult();
          runtimeService.trigger(receiveExecution.getId());
          assertProcessEnded(processInstance.getId());
          assertThat(runtimeService.createExecutionQuery().count()).isEqualTo(0);
      }
  
      @Deployment
      public void testNestedCancelInner() {
          ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess");
  
          // after the process is started, we have compensate event subscriptions:
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()
          ).isEqualTo(0);
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookHotel").count()
          ).isEqualTo(5);
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookFlight").count()
          ).isEqualTo(1);
  
          // the tasks are present:
          Task taskInner = taskService.createTaskQuery().taskDefinitionKey("innerTxaskCustomer").singleResult();
          Task taskOuter = taskService.createTaskQuery().taskDefinitionKey("bookFlight").singleResult();
          assertThat(taskInner).isNotNull();
          assertThat(taskOuter).isNotNull();
  
          // making the tx fail:
          taskService.setVariable(taskInner.getId(), "confirmed", false);
          taskService.complete(taskInner.getId());
  
          // now the process instance execution is sitting in the
          // 'afterInnerCancellation' task
          // -> has left the transaction using the cancel boundary event
          List<String> activeActivityIds = runtimeService.getActiveActivityIds(processInstance.getId());
          assertThat(activeActivityIds.contains("afterInnerCancellation")).isTrue();
  
          // we have no more compensate event subscriptions for the inner tx
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookHotel").count()
          ).isEqualTo(0);
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookFlight").count()
          ).isEqualTo(0);
  
          // we do not have a subscription or the outer tx yet
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()
          ).isEqualTo(0);
  
          // assert that the compensation handlers have been invoked:
          assertThat(runtimeService.getVariable(processInstance.getId(), "innerTxundoBookHotel")).isEqualTo(5);
          assertThat(runtimeService.getVariable(processInstance.getId(), "innerTxundoBookFlight")).isEqualTo(1);
  
          // if we have history, we check that the invocation of the compensation handlers is recorded in history.
          if (processEngineConfiguration.getHistoryLevel().isAtLeast(HistoryLevel.ACTIVITY)) {
              assertThat(
                  historyService.createHistoricActivityInstanceQuery().activityId("innerTxundoBookHotel").count()
              ).isEqualTo(5);
              assertThat(
                  historyService.createHistoricActivityInstanceQuery().activityId("innerTxundoBookFlight").count()
              ).isEqualTo(1);
          }
  
          // complete the task in the outer tx
          taskService.complete(taskOuter.getId());
  
          // end the process instance (signal the execution still sitting in afterInnerCancellation)
          runtimeService.trigger(
              runtimeService.createExecutionQuery().activityId("afterInnerCancellation").singleResult().getId()
          );
  
          assertProcessEnded(processInstance.getId());
          assertThat(runtimeService.createExecutionQuery().count()).isEqualTo(0);
      }
  
      @Deployment
      public void testNestedCancelOuter() {
          ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess");
  
          // after the process is started, we have compensate event subscriptions:
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()
          ).isEqualTo(0);
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookHotel").count()
          ).isEqualTo(5);
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookFlight").count()
          ).isEqualTo(1);
  
          // the tasks are present:
          Task taskInner = taskService.createTaskQuery().taskDefinitionKey("innerTxaskCustomer").singleResult();
          Task taskOuter = taskService.createTaskQuery().taskDefinitionKey("bookFlight").singleResult();
          assertThat(taskInner).isNotNull();
          assertThat(taskOuter).isNotNull();
  
          // making the outer tx fail (invokes cancel end event)
          taskService.complete(taskOuter.getId());
  
          // now the process instance is sitting in 'afterOuterCancellation'
          List<String> activeActivityIds = runtimeService.getActiveActivityIds(processInstance.getId());
          assertThat(activeActivityIds.contains("afterOuterCancellation")).isTrue();
  
          // we have no more compensate event subscriptions
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookHotel").count()
          ).isEqualTo(0);
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("innerTxundoBookFlight").count()
          ).isEqualTo(0);
          assertThat(
              createEventSubscriptionQuery().eventType("compensate").activityId("undoBookFlight").count()
          ).isEqualTo(0);
  
          // the compensation handlers of the inner tx have not been invoked
          assertThat(runtimeService.getVariable(processInstance.getId(), "innerTxundoBookHotel")).isNull();
          assertThat(runtimeService.getVariable(processInstance.getId(), "innerTxundoBookFlight")).isNull();
  
          // the compensation handler in the outer tx has been invoked
          assertThat(runtimeService.getVariable(processInstance.getId(), "undoBookFlight")).isEqualTo(1);
  
          // end the process instance (signal the execution still sitting in afterOuterCancellation)
          runtimeService.trigger(
              runtimeService.createExecutionQuery().activityId("afterOuterCancellation").singleResult().getId()
          );
  
          assertProcessEnded(processInstance.getId());
          assertThat(runtimeService.createExecutionQuery().count()).isEqualTo(0);
      }
  
      /*
       * The cancel end event cancels all instances, compensation is performed for all instances
       *
       * see spec page 470: "If the cancelActivity attribute is set, the Activity the Event is attached to is then cancelled (in case of a multi-instance, all its instances are cancelled);"
       */
      @Deployment
      public void testMultiInstanceTx() {
          ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess");
  
          // there are now 5 instances of the transaction:
  
          List<EventSubscriptionEntity> eventSubscriptionEntities = createEventSubscriptionQuery()
              .eventType("compensate")
              .list();
  
          // there are 10 compensation event subscriptions
          assertThat(eventSubscriptionEntities).hasSize(10);
  
          Task task = taskService.createTaskQuery().listPage(0, 1).get(0);
  
          // canceling one instance triggers compensation for all other instances:
          taskService.setVariable(task.getId(), "confirmed", false);
          taskService.complete(task.getId());
  
          assertThat(createEventSubscriptionQuery().count()).isEqualTo(0);
  
          assertThat(runtimeService.getVariable(processInstance.getId(), "undoBookHotel")).isEqualTo(5);
          assertThat(runtimeService.getVariable(processInstance.getId(), "undoBookFlight")).isEqualTo(5);
  
          runtimeService.trigger(
              runtimeService.createExecutionQuery().activityId("afterCancellation").singleResult().getId()
          );
  
          assertProcessEnded(processInstance.getId());
      }
  
      @Deployment(
          resources = {
              "org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testMultiInstanceTx.bpmn20.xml",
          }
      )
      public void testMultiInstanceTxSuccessful() {
          ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("transactionProcess");
  
          // there are now 5 instances of the transaction:
  
          List<EventSubscriptionEntity> EventSubscriptionEntitys = createEventSubscriptionQuery()
              .eventType("compensate")
              .list();
  
          // there are 10 compensation event subscriptions
          assertThat(EventSubscriptionEntitys).hasSize(10);
  
          // first complete the inner user-tasks
          List<Task> tasks = taskService.createTaskQuery().list();
          for (Task task : tasks) {
              taskService.setVariable(task.getId(), "confirmed", true);
              taskService.complete(task.getId());
          }
  
          // now complete the inner receive tasks
          List<Execution> executions = runtimeService.createExecutionQuery().activityId("receive").list();
          for (Execution execution : executions) {
              runtimeService.trigger(execution.getId());
          }
  
          runtimeService.trigger(runtimeService.createExecutionQuery().activityId("afterSuccess").singleResult().getId());
  
          assertThat(createEventSubscriptionQuery().count()).isEqualTo(0);
          assertProcessEnded(processInstance.getId());
      }
  
      public void testMultipleCancelBoundaryFails() {
          assertThatExceptionOfType(ActivitiException.class)
              .isThrownBy(() ->
                  repositoryService
                      .createDeployment()
                      .addClasspathResource(
                          "org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testMultipleCancelBoundaryFails.bpmn20.xml"
                      )
                      .deploy()
              )
              .withMessageContaining(
                  "multiple boundary events with cancelEventDefinition not supported on same transaction"
              );
      }
  
      public void testCancelBoundaryNoTransactionFails() {
          assertThatExceptionOfType(Exception.class)
              .isThrownBy(() ->
                  repositoryService
                      .createDeployment()
                      .addClasspathResource(
                          "org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testCancelBoundaryNoTransactionFails.bpmn20.xml"
                      )
                      .deploy()
              )
              .withMessageContaining(
                  "boundary event with cancelEventDefinition only supported on transaction subprocesses"
              );
      }
  
      public void testCancelEndNoTransactionFails() {
          assertThatExceptionOfType(Exception.class)
              .isThrownBy(() ->
                  repositoryService
                      .createDeployment()
                      .addClasspathResource(
                          "org/activiti/engine/test/bpmn/subprocess/transaction/TransactionSubProcessTest.testCancelEndNoTransactionFails.bpmn20.xml"
                      )
                      .deploy()
              )
              .withMessageContaining("end event with cancelEventDefinition only supported inside transaction subprocess");
      }
  
      private EventSubscriptionQueryImpl createEventSubscriptionQuery() {
          return new EventSubscriptionQueryImpl(processEngineConfiguration.getCommandExecutor());
      }
  
      @Deployment
      public void testParseWithDI() {
          // this test simply makes sure we can parse a transaction subprocess with DI information
          // the actual transaction behavior is tested by other test cases
  
          // failing case
  
          ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("TransactionSubProcessTest");
  
          Task task = taskService.createTaskQuery().singleResult();
          taskService.setVariable(task.getId(), "confirmed", false);
  
          taskService.complete(task.getId());
  
          assertProcessEnded(processInstance.getId());
  
          // success case
  
          processInstance = runtimeService.startProcessInstanceByKey("TransactionSubProcessTest");
  
          task = taskService.createTaskQuery().singleResult();
          taskService.setVariable(task.getId(), "confirmed", true);
  
          taskService.complete(task.getId());
  
          assertProcessEnded(processInstance.getId());
      }
  }
  