Compare commits

9 Commits

Author SHA1 Message Date
Luc Weinbrecht
248b1c5d2f update readme (mentioned blog post) 2022-06-27 06:27:01 +02:00
Luc Weinbrecht
28c36a589d removed unnecessary fetch variable specification 2022-06-27 06:20:12 +02:00
Luc Weinbrecht
817c683ad5 autocompleting tasks 2022-06-24 06:19:14 +02:00
Luc Weinbrecht
2186248b8b added spring boot process test 2022-06-19 09:36:31 +02:00
Luc Weinbrecht
5b5f81b3f8 setting business key for loan agreement 2022-06-19 09:36:31 +02:00
Luc Weinbrecht
48ee99e182 removed zeebe spring boot tests and outsource process test utils 2022-06-19 09:36:31 +02:00
Luc Weinbrecht
fda0bb30bf building camunda 8 branch as well 2022-06-19 09:36:31 +02:00
Luc Weinbrecht
ec62e6e3c9 added zeebe extension tests 2022-06-19 09:36:31 +02:00
Luc Weinbrecht
00bfe0fb81 switched to camunda 8 2022-06-19 09:36:29 +02:00
42 changed files with 1022 additions and 544 deletions

View File

@@ -4,6 +4,7 @@ on:
push: push:
branches: branches:
- 'main' - 'main'
- 'camunda-8'
jobs: jobs:
build-test-utils: build-test-utils:
@@ -76,9 +77,9 @@ jobs:
- name: Login to Docker Hub - name: Login to Docker Hub
run: docker login -u ${{ secrets.DOCKER_USER }} -p '${{ secrets.DOCKER_TOKEN }}' run: docker login -u ${{ secrets.DOCKER_USER }} -p '${{ secrets.DOCKER_TOKEN }}'
- name: Build Docker image - name: Build Docker image
run: docker build -t ${{env.repo}}-loan-agreement . run: docker build -t ${{env.repo}}-loan-agreement-camunda-8 .
- name: Publish Docker image - name: Publish Docker image
run: docker push ${{env.repo}}-loan-agreement run: docker push ${{env.repo}}-loan-agreement-camunda-8
recommendation-build: recommendation-build:
name: Build Recommendation JAR name: Build Recommendation JAR
@@ -131,6 +132,6 @@ jobs:
- name: Login to Docker Hub - name: Login to Docker Hub
run: docker login -u ${{ secrets.DOCKER_USER }} -p '${{ secrets.DOCKER_TOKEN }}' run: docker login -u ${{ secrets.DOCKER_USER }} -p '${{ secrets.DOCKER_TOKEN }}'
- name: Build Docker image - name: Build Docker image
run: docker build -t ${{env.repo}}-recommendation . run: docker build -t ${{env.repo}}-recommendation-camunda-8 .
- name: Publish Docker image - name: Publish Docker image
run: docker push ${{env.repo}}-recommendation run: docker push ${{env.repo}}-recommendation-camunda-8

View File

@@ -2,10 +2,25 @@
An example to show how you could use clean architecture and DDD and their advantages with Camunda. An example to show how you could use clean architecture and DDD and their advantages with Camunda.
I also wrote a blog post to show how clean architecture could help you to update to Camunda Platform 8 without
touching your domain centered code: [How Clean Architecture helps you migrating Camunda Platform 7 to 8](https://www.novatec-gmbh.de/en/blog/how-clean-architecture-helps-you-migrating-camunda-platform-7-to-8/).
## 🚀Features ## 🚀Features
The [BPMN process](assets/processes/loan_agreement.png) which start a [second process](assets/processes/cross_selling_recommendation.png) via message correlation should represent a tiny business process just to demonstrate the architecture. The [BPMN process](assets/processes/loan_agreement.png) which start a [second process](assets/processes/cross_selling_recommendation.png) via message correlation should represent a tiny business process just to demonstrate the architecture.
Configure your [Camunda Platform 8 SaaS](https://camunda.com/get-started/) to connect to using the [following properties](https://github.com/camunda-community-hub/spring-zeebe#configuring-camunda-platform-8-saas-connection) .
If you want to run [Zeebe locally](https://docs.camunda.io/docs/self-managed/platform-deployment/kubernetes-helm/) you need to use the following properties ([source](https://github.com/camunda-community-hub/spring-zeebe#configuring-camunda-platform-8-saas-connection)):
```yaml
zeebe:
client:
broker:
gateway-address: localhost:26500
security:
plaintext: true
```
### 🛫Start the process ### 🛫Start the process
With the following POST request, you could start the process: With the following POST request, you could start the process:

View File

@@ -15,7 +15,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<version.springboot>2.6.4</version.springboot> <version.springboot>2.6.4</version.springboot>
<version.camunda>7.17.0</version.camunda> <version.zeebe>8.0.1</version.zeebe>
<version.junit5>5.8.2</version.junit5> <version.junit5>5.8.2</version.junit5>
<version.lombok>1.18.24</version.lombok> <version.lombok>1.18.24</version.lombok>
<version.domainprimitives>0.1.0</version.domainprimitives> <version.domainprimitives>0.1.0</version.domainprimitives>
@@ -37,9 +37,9 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.camunda.bpm</groupId> <groupId>io.camunda</groupId>
<artifactId>camunda-bom</artifactId> <artifactId>zeebe-bom</artifactId>
<version>${version.camunda}</version> <version>${version.zeebe}</version>
<scope>import</scope> <scope>import</scope>
<type>pom</type> <type>pom</type>
</dependency> </dependency>
@@ -48,23 +48,35 @@
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.camunda.bpm.springboot</groupId> <groupId>io.camunda</groupId>
<artifactId>camunda-bpm-spring-boot-starter-rest</artifactId> <artifactId>spring-zeebe</artifactId>
<version>${version.zeebe}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.camunda.bpm.springboot</groupId> <groupId>io.camunda</groupId>
<artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId> <artifactId>spring-zeebe-starter</artifactId>
<version>${version.zeebe}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>io.camunda</groupId>
<artifactId>spring-boot-starter-webflux</artifactId> <artifactId>spring-zeebe-util</artifactId>
<version>${version.zeebe}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.camunda.bpm.springboot</groupId> <groupId>io.camunda</groupId>
<artifactId>camunda-bpm-spring-boot-starter-test</artifactId> <artifactId>spring-zeebe-test</artifactId>
<version>${version.zeebe}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.camunda</groupId>
<artifactId>zeebe-process-test-extension</artifactId>
<version>${version.zeebe}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -72,6 +84,11 @@
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>
<artifactId>h2</artifactId> <artifactId>h2</artifactId>
@@ -118,21 +135,6 @@
<version>${version.junit5}</version> <version>${version.junit5}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.camunda.bpm.extension</groupId>
<artifactId>camunda-bpm-junit5</artifactId>
<version>${version.bpmAssert}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.camunda.community.mockito</groupId>
<artifactId>camunda-platform-7-mockito</artifactId>
<version>${version.camundaMockito}</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>com.squareup.okhttp3</groupId> <groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId> <artifactId>okhttp</artifactId>
@@ -164,6 +166,14 @@
<build> <build>
<finalName>${project.artifactId}</finalName> <finalName>${project.artifactId}</finalName>
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
</testResource>
<testResource>
<directory>${project.basedir}/src/main/resources</directory>
</testResource>
</testResources>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@@ -0,0 +1,11 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement;
import io.camunda.zeebe.spring.client.EnableZeebeClient;
import io.camunda.zeebe.spring.client.annotation.ZeebeDeployment;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableZeebeClient
@ZeebeDeployment(resources = {"classpath:*.bpmn", "classpath:*.dmn"})
public class ApplicationConfiguration {
}

View File

@@ -6,6 +6,10 @@ public class ProcessConstants {
public static final String LOAN_AGREEMENT_NUMBER = "loanAgreementNumber"; public static final String LOAN_AGREEMENT_NUMBER = "loanAgreementNumber";
public static final String RECOMMENDATION_START_EVENT_MESSAGE_REF = "crossSellingPotentialDiscoveredMessage"; public static final String RECOMMENDATION_START_EVENT_MESSAGE_REF = "crossSellingPotentialDiscoveredMessage";
public static final String RECOMMENDATION_CUSTOMER_NUMBER = "customerNumber"; public static final String RECOMMENDATION_CUSTOMER_NUMBER = "customerNumber";
public static final String BUSINESS_KEY = "businessKey";
public static final String LOAN_AGREEMENT_TASK = "approve-loan-agreement";
public static final String LOAN_REJECTION_TASK = "reject-loan-agreement";
public static final String SEND_CROSS_SELLING_RECOMMENDATION_TASK = "send-cross-selling-recommendation";
} }

View File

@@ -2,24 +2,23 @@ package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.process;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber; import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in.LoanAgreementStatusCommand; import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in.LoanAgreementStatusCommand;
import io.camunda.zeebe.spring.client.annotation.ZeebeVariable;
import io.camunda.zeebe.spring.client.annotation.ZeebeWorker;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_AGREEMENT_NUMBER; import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_AGREEMENT_TASK;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@Component @Component
public class ApproveLoanAgreement implements JavaDelegate { public class ApproveLoanAgreement {
private final LoanAgreementStatusCommand loanAgreementStatusCommand; private final LoanAgreementStatusCommand loanAgreementStatusCommand;
@Override @ZeebeWorker(type = LOAN_AGREEMENT_TASK, autoComplete = true)
public void execute(DelegateExecution delegateExecution) { public void handleJobFoo(@ZeebeVariable Number loanAgreementNumber) {
Long loanAgreementNumber = (Long) delegateExecution.getVariable(LOAN_AGREEMENT_NUMBER); loanAgreementStatusCommand.accept(new LoanAgreementNumber(loanAgreementNumber.longValue()));
loanAgreementStatusCommand.accept(new LoanAgreementNumber(loanAgreementNumber));
} }
} }

View File

@@ -2,24 +2,23 @@ package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.process;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber; import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in.LoanAgreementStatusCommand; import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in.LoanAgreementStatusCommand;
import io.camunda.zeebe.spring.client.annotation.ZeebeVariable;
import io.camunda.zeebe.spring.client.annotation.ZeebeWorker;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_AGREEMENT_NUMBER; import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_REJECTION_TASK;
@Slf4j @Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@Component @Component
public class RejectionLoanAgreement implements JavaDelegate { public class RejectionLoanAgreement {
private final LoanAgreementStatusCommand loanAgreementStatusCommand; private final LoanAgreementStatusCommand loanAgreementStatusCommand;
@Override @ZeebeWorker(type = LOAN_REJECTION_TASK, autoComplete = true)
public void execute(DelegateExecution delegateExecution) { public void handleJobFoo(@ZeebeVariable Number loanAgreementNumber) {
Long loanAgreementNumber = (Long) delegateExecution.getVariable(LOAN_AGREEMENT_NUMBER); loanAgreementStatusCommand.reject(new LoanAgreementNumber(loanAgreementNumber.longValue()));
loanAgreementStatusCommand.reject(new LoanAgreementNumber(loanAgreementNumber));
} }
} }

View File

@@ -5,25 +5,26 @@ import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreem
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber; import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementQuery; import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementQuery;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.RecommendationTrigger; import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.RecommendationTrigger;
import io.camunda.zeebe.spring.client.annotation.ZeebeVariable;
import io.camunda.zeebe.spring.client.annotation.ZeebeWorker;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_AGREEMENT_NUMBER; import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.SEND_CROSS_SELLING_RECOMMENDATION_TASK;
@RequiredArgsConstructor @RequiredArgsConstructor
@Component @Component
public class SendCrossSellingRecommendation implements JavaDelegate { public class SendCrossSellingRecommendation {
private final RecommendationTrigger recommendationTrigger; private final RecommendationTrigger recommendationTrigger;
private final LoanAgreementQuery loanAgreementQuery; private final LoanAgreementQuery loanAgreementQuery;
@Override @ZeebeWorker(type = SEND_CROSS_SELLING_RECOMMENDATION_TASK, autoComplete = true)
public void execute(DelegateExecution execution) { public void handleJobFoo(@ZeebeVariable Number loanAgreementNumber, @ZeebeVariable String businessKey) {
Long loanNumber = (Long) execution.getVariable(LOAN_AGREEMENT_NUMBER); LoanAgreement loanAgreement = loanAgreementQuery.loadByNumber(
LoanAgreement loanAgreement = loanAgreementQuery.loadByNumber(new LoanAgreementNumber(loanNumber)); new LoanAgreementNumber(loanAgreementNumber.longValue())
);
recommendationTrigger.startLoanAgreement(new CaseId(execution.getBusinessKey()), loanAgreement); recommendationTrigger.startLoanAgreement(new CaseId(businessKey), loanAgreement);
} }
} }

View File

@@ -0,0 +1,7 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.process;
public class CouldNotPublishMessageException extends RuntimeException {
public CouldNotPublishMessageException(Throwable cause) {
super(cause);
}
}

View File

@@ -3,26 +3,33 @@ package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.process;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.CaseId; import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.CaseId;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber; import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.WorkflowCommand; import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.WorkflowCommand;
import io.camunda.zeebe.client.ZeebeClient;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.camunda.bpm.engine.RuntimeService;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_AGREEMENT_NUMBER; import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.*;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_START_EVENT_MESSAGE_REF;
@RequiredArgsConstructor @RequiredArgsConstructor
@Component @Component
class ProcessEngineClient implements WorkflowCommand { class ProcessEngineClient implements WorkflowCommand {
private final RuntimeService runtimeService; private final ZeebeClient client;
@Override @Override
public void startLoanAgreement(CaseId caseId, LoanAgreementNumber loanAgreementNumber) { public void startLoanAgreement(CaseId caseId, LoanAgreementNumber loanAgreementNumber) {
Map<String, Object> processVariables = new HashMap<>(); Map<String, Object> processVariables = new HashMap<>();
processVariables.put(LOAN_AGREEMENT_NUMBER, loanAgreementNumber.getValue()); processVariables.put(LOAN_AGREEMENT_NUMBER, loanAgreementNumber.getValue());
runtimeService.startProcessInstanceByMessage(LOAN_START_EVENT_MESSAGE_REF, caseId.getValue(), processVariables); processVariables.put(BUSINESS_KEY, caseId.getValue());
client.newPublishMessageCommand()
.messageName(LOAN_START_EVENT_MESSAGE_REF)
.correlationKey("")
.variables(processVariables)
.send()
.exceptionally(throwable -> {
throw new CouldNotPublishMessageException(throwable);
});
} }
} }

View File

@@ -5,9 +5,9 @@ spring:
hibernate: hibernate:
ddl-auto: create-drop ddl-auto: create-drop
camunda.bpm.admin-user: zeebe:
id: admin client:
password: pw cloud:
generic-properties: cluster-id: <your cluster ID>
properties: client-id: <your client ID>
initializeTelemetry: false client-secret: <you secret>

View File

@@ -1,26 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/" xmlns:dmndi="https://www.omg.org/spec/DMN/20191111/DMNDI/" xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/" xmlns:modeler="http://camunda.org/schema/modeler/1.0" xmlns:biodi="http://bpmn.io/schema/dmn/biodi/2.0" xmlns:camunda="http://camunda.org/schema/1.0/dmn" id="Definitions_11dxtis" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="5.0.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.17.0"> <definitions xmlns="https://www.omg.org/spec/DMN/20191111/MODEL/" xmlns:dmndi="https://www.omg.org/spec/DMN/20191111/DMNDI/" xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_0ig9g89" name="DRD" namespace="http://camunda.org/schema/1.0/dmn" exporter="Camunda Modeler" exporterVersion="5.0.0" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.0.0">
<decision id="approvement-check" name="Approve Loan Agreement"> <decision id="approvement-check" name="Approve Loan Agreement">
<decisionTable id="DecisionTable_0ffqkch"> <decisionTable id="DecisionTable_0ojcgph">
<input id="Input_1" biodi:width="192" camunda:inputVariable="loanAgreementNumber"> <input id="Input_1">
<inputExpression id="InputExpression_1" typeRef="integer"> <inputExpression id="InputExpression_1" typeRef="number">
<text>loanAgreementNumber</text> <text>loanAgreementNumber</text>
</inputExpression> </inputExpression>
</input> </input>
<output id="Output_1" name="approved" typeRef="boolean" biodi:width="192" /> <output id="Output_1" name="approved" typeRef="boolean" />
<rule id="DecisionRule_0p9ijdl"> <rule id="DecisionRule_1ofiexe">
<inputEntry id="UnaryTests_1qou5p9"> <inputEntry id="UnaryTests_0xwmkci">
<text>&gt;= 5</text> <text>&gt;= 5</text>
</inputEntry> </inputEntry>
<outputEntry id="LiteralExpression_1hlf608"> <outputEntry id="LiteralExpression_0i1nxma">
<text>false</text> <text>false</text>
</outputEntry> </outputEntry>
</rule> </rule>
<rule id="DecisionRule_1arucld"> <rule id="DecisionRule_0t4w3lo">
<inputEntry id="UnaryTests_1vqubek"> <inputEntry id="UnaryTests_1vli5s3">
<text>&lt; 5</text> <text>&lt; 5</text>
</inputEntry> </inputEntry>
<outputEntry id="LiteralExpression_1ovr1wt"> <outputEntry id="LiteralExpression_1puh4m4">
<text>true</text> <text>true</text>
</outputEntry> </outputEntry>
</rule> </rule>

View File

@@ -1,157 +1,166 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1yngi5u" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.17.0"> <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_1cojsk2" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.0.0">
<bpmn:collaboration id="Collaboration_0w4ikjr"> <bpmn:collaboration id="Collaboration_0w35pbw">
<bpmn:participant id="Participant_1p6ilgg" name="Loan Agreement example" processRef="Loan_Agreement" /> <bpmn:participant id="Participant_16ksm4o" name="Loan Agreement example" processRef="Loan_Agreement" />
<bpmn:participant id="Participant_0y932pu" name="Third-party-legacy System" /> <bpmn:participant id="Participant_1xcghje" name="Third-party-legacy System" />
<bpmn:participant id="Participant_0z5fkq5" name="Third-party-legacy System" /> <bpmn:participant id="Participant_10a5hxf" name="Third-party-legacy System" />
<bpmn:messageFlow id="Flow_1ytdopc" sourceRef="RejectLoanAgreementServiceTask" targetRef="Participant_0y932pu" /> <bpmn:messageFlow id="Flow_1a4toos" sourceRef="ApproveLoanAgreementServiceTask" targetRef="Participant_1xcghje" />
<bpmn:messageFlow id="Flow_1ax0a41" sourceRef="ApproveLoanAgreementServiceTask" targetRef="Participant_0z5fkq5" /> <bpmn:messageFlow id="Flow_1sr6eeg" sourceRef="RejectLoanAgreementServiceTask" targetRef="Participant_10a5hxf" />
</bpmn:collaboration> </bpmn:collaboration>
<bpmn:process id="Loan_Agreement" name="Loan Agreement" isExecutable="true"> <bpmn:process id="Loan_Agreement" name="Loan Agreement" isExecutable="true">
<bpmn:businessRuleTask id="ApproveAgreementRuleTask" name="Approve agreement" camunda:resultVariable="approved" camunda:decisionRef="approvement-check" camunda:mapDecisionResult="singleEntry"> <bpmn:startEvent id="LoanAgreementReceivedStartEvent" name="loan agreement recived">
<bpmn:extensionElements /> <bpmn:outgoing>Flow_1a7dewt</bpmn:outgoing>
<bpmn:incoming>Flow_1rrsueh</bpmn:incoming> <bpmn:messageEventDefinition id="MessageEventDefinition_1c5v0o8" messageRef="Message_0o9ohqs" />
<bpmn:outgoing>Flow_00ukhfv</bpmn:outgoing>
</bpmn:businessRuleTask>
<bpmn:exclusiveGateway id="IsApprovedGateway" name="Is agreement approved?">
<bpmn:incoming>Flow_00ukhfv</bpmn:incoming>
<bpmn:outgoing>Flow_0xpo6jp</bpmn:outgoing>
<bpmn:outgoing>Flow_1hri7xc</bpmn:outgoing>
</bpmn:exclusiveGateway>
<bpmn:serviceTask id="ApproveLoanAgreementServiceTask" name="Approve loan agreement" camunda:delegateExpression="${approveLoanAgreement}">
<bpmn:incoming>Flow_1hri7xc</bpmn:incoming>
<bpmn:outgoing>Flow_1uqmps7</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:startEvent id="LoanAgreementReciedStartEvent" name="loan agreement recived" camunda:asyncAfter="true">
<bpmn:outgoing>Flow_1rrsueh</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_1j9r08u" messageRef="Message_0o102df" />
</bpmn:startEvent> </bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1uqmps7" sourceRef="ApproveLoanAgreementServiceTask" targetRef="SendCrossSellingEvent" /> <bpmn:businessRuleTask id="ApproveAgreementRuleTask" name="Approve agreement">
<bpmn:sequenceFlow id="Flow_1hri7xc" name="yes" sourceRef="IsApprovedGateway" targetRef="ApproveLoanAgreementServiceTask"> <bpmn:extensionElements>
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${approved}</bpmn:conditionExpression> <zeebe:calledDecision decisionId="approvement-check" resultVariable="approved" />
</bpmn:sequenceFlow> </bpmn:extensionElements>
<bpmn:sequenceFlow id="Flow_0xpo6jp" name="no" sourceRef="IsApprovedGateway" targetRef="RejectLoanAgreementServiceTask"> <bpmn:incoming>Flow_1a7dewt</bpmn:incoming>
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${!approved}</bpmn:conditionExpression> <bpmn:outgoing>Flow_0b8w6r4</bpmn:outgoing>
</bpmn:sequenceFlow> </bpmn:businessRuleTask>
<bpmn:sequenceFlow id="Flow_00ukhfv" sourceRef="ApproveAgreementRuleTask" targetRef="IsApprovedGateway" /> <bpmn:exclusiveGateway id="Gateway_1lbkm1m" name="Is agreement approved?">
<bpmn:sequenceFlow id="Flow_1rrsueh" sourceRef="LoanAgreementReciedStartEvent" targetRef="ApproveAgreementRuleTask" /> <bpmn:incoming>Flow_0b8w6r4</bpmn:incoming>
<bpmn:serviceTask id="RejectLoanAgreementServiceTask" name="Rejection loan agreement" camunda:delegateExpression="${rejectionLoanAgreement}"> <bpmn:outgoing>Flow_11ck3o3</bpmn:outgoing>
<bpmn:incoming>Flow_0xpo6jp</bpmn:incoming> <bpmn:outgoing>Flow_1x7d6jb</bpmn:outgoing>
<bpmn:outgoing>Flow_0eif63m</bpmn:outgoing> </bpmn:exclusiveGateway>
<bpmn:serviceTask id="ApproveLoanAgreementServiceTask" name="Approve loan agreement">
<bpmn:extensionElements>
<zeebe:taskDefinition type="approve-loan-agreement" />
</bpmn:extensionElements>
<bpmn:incoming>Flow_11ck3o3</bpmn:incoming>
<bpmn:outgoing>Flow_1m68173</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:serviceTask id="RejectLoanAgreementServiceTask" name="Rejection loan agreement">
<bpmn:extensionElements>
<zeebe:taskDefinition type="reject-loan-agreement" />
</bpmn:extensionElements>
<bpmn:incoming>Flow_1x7d6jb</bpmn:incoming>
<bpmn:outgoing>Flow_0u06ha5</bpmn:outgoing>
</bpmn:serviceTask> </bpmn:serviceTask>
<bpmn:sequenceFlow id="Flow_0eif63m" sourceRef="RejectLoanAgreementServiceTask" targetRef="LoanAgreementNotApprovedEndEvent" />
<bpmn:endEvent id="LoanAgreementApprovedEndEvent" name="loan agreement approved">
<bpmn:incoming>Flow_1pvaqlv</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1pvaqlv" sourceRef="SendCrossSellingEvent" targetRef="LoanAgreementApprovedEndEvent" />
<bpmn:intermediateThrowEvent id="SendCrossSellingEvent" name="Send cross-selling recommendaiton">
<bpmn:incoming>Flow_1uqmps7</bpmn:incoming>
<bpmn:outgoing>Flow_1pvaqlv</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_0drw25h" camunda:delegateExpression="${sendCrossSellingRecommendation}" />
</bpmn:intermediateThrowEvent>
<bpmn:endEvent id="LoanAgreementNotApprovedEndEvent" name="loan agreement not approved"> <bpmn:endEvent id="LoanAgreementNotApprovedEndEvent" name="loan agreement not approved">
<bpmn:incoming>Flow_0eif63m</bpmn:incoming> <bpmn:incoming>Flow_0u06ha5</bpmn:incoming>
</bpmn:endEvent> </bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1a7dewt" sourceRef="LoanAgreementReceivedStartEvent" targetRef="ApproveAgreementRuleTask" />
<bpmn:sequenceFlow id="Flow_0b8w6r4" sourceRef="ApproveAgreementRuleTask" targetRef="Gateway_1lbkm1m" />
<bpmn:sequenceFlow id="Flow_11ck3o3" name="yes" sourceRef="Gateway_1lbkm1m" targetRef="ApproveLoanAgreementServiceTask">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=approved=true</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_1m68173" sourceRef="ApproveLoanAgreementServiceTask" targetRef="SendCrossSellingEvent" />
<bpmn:sequenceFlow id="Flow_1x7d6jb" name="no" sourceRef="Gateway_1lbkm1m" targetRef="RejectLoanAgreementServiceTask">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">=approved=false</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_0u06ha5" sourceRef="RejectLoanAgreementServiceTask" targetRef="LoanAgreementNotApprovedEndEvent" />
<bpmn:intermediateThrowEvent id="SendCrossSellingEvent" name="Send cross-selling recommendaiton">
<bpmn:extensionElements>
<zeebe:taskDefinition type="send-cross-selling-recommendation" />
</bpmn:extensionElements>
<bpmn:incoming>Flow_1m68173</bpmn:incoming>
<bpmn:outgoing>Flow_04mwr0b</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_1x5iaae" />
</bpmn:intermediateThrowEvent>
<bpmn:endEvent id="LoanAgreementApprovedEndEvent" name="loan agreement approved">
<bpmn:incoming>Flow_04mwr0b</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_04mwr0b" sourceRef="SendCrossSellingEvent" targetRef="LoanAgreementApprovedEndEvent" />
</bpmn:process> </bpmn:process>
<bpmn:message id="Message_0o102df" name="loanAgreementReceivedMessage" /> <bpmn:message id="Message_0o9ohqs" name="loanAgreementReceivedMessage" />
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0w4ikjr"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0w35pbw">
<bpmndi:BPMNShape id="Participant_1p6ilgg_di" bpmnElement="Participant_1p6ilgg" isHorizontal="true"> <bpmndi:BPMNShape id="Participant_16ksm4o_di" bpmnElement="Participant_16ksm4o" isHorizontal="true">
<dc:Bounds x="160" y="177" width="820" height="255" /> <dc:Bounds x="160" y="170" width="860" height="270" />
<bpmndi:BPMNLabel /> <bpmndi:BPMNLabel />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1rrsueh_di" bpmnElement="Flow_1rrsueh"> <bpmndi:BPMNEdge id="Flow_04mwr0b_di" bpmnElement="Flow_04mwr0b">
<di:waypoint x="258" y="237" /> <di:waypoint x="838" y="250" />
<di:waypoint x="330" y="237" /> <di:waypoint x="936" y="250" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_00ukhfv_di" bpmnElement="Flow_00ukhfv"> <bpmndi:BPMNEdge id="Flow_0u06ha5_di" bpmnElement="Flow_0u06ha5">
<di:waypoint x="430" y="237" /> <di:waypoint x="710" y="360" />
<di:waypoint x="485" y="237" /> <di:waypoint x="802" y="360" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0xpo6jp_di" bpmnElement="Flow_0xpo6jp"> <bpmndi:BPMNEdge id="Flow_1x7d6jb_di" bpmnElement="Flow_1x7d6jb">
<di:waypoint x="510" y="262" /> <di:waypoint x="500" y="275" />
<di:waypoint x="510" y="360" /> <di:waypoint x="500" y="360" />
<di:waypoint x="600" y="360" /> <di:waypoint x="610" y="360" />
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="519" y="308" width="13" height="14" /> <dc:Bounds x="509" y="315" width="13" height="14" />
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1hri7xc_di" bpmnElement="Flow_1hri7xc"> <bpmndi:BPMNEdge id="Flow_1m68173_di" bpmnElement="Flow_1m68173">
<di:waypoint x="535" y="237" /> <di:waypoint x="710" y="250" />
<di:waypoint x="600" y="237" /> <di:waypoint x="802" y="250" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_11ck3o3_di" bpmnElement="Flow_11ck3o3">
<di:waypoint x="525" y="250" />
<di:waypoint x="610" y="250" />
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="559" y="219" width="18" height="14" /> <dc:Bounds x="559" y="232" width="18" height="14" />
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1uqmps7_di" bpmnElement="Flow_1uqmps7"> <bpmndi:BPMNEdge id="Flow_0b8w6r4_di" bpmnElement="Flow_0b8w6r4">
<di:waypoint x="700" y="237" /> <di:waypoint x="420" y="250" />
<di:waypoint x="772" y="237" /> <di:waypoint x="475" y="250" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0eif63m_di" bpmnElement="Flow_0eif63m"> <bpmndi:BPMNEdge id="Flow_1a7dewt_di" bpmnElement="Flow_1a7dewt">
<di:waypoint x="700" y="360" /> <di:waypoint x="268" y="250" />
<di:waypoint x="772" y="360" /> <di:waypoint x="320" y="250" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1pvaqlv_di" bpmnElement="Flow_1pvaqlv"> <bpmndi:BPMNShape id="Event_189g024_di" bpmnElement="LoanAgreementReceivedStartEvent">
<di:waypoint x="808" y="237" /> <dc:Bounds x="232" y="232" width="36" height="36" />
<di:waypoint x="882" y="237" /> <bpmndi:BPMNLabel>
</bpmndi:BPMNEdge> <dc:Bounds x="212" y="275" width="77" height="27" />
<bpmndi:BPMNShape id="Activity_1244zx8_di" bpmnElement="ApproveAgreementRuleTask"> </bpmndi:BPMNLabel>
<dc:Bounds x="330" y="197" width="100" height="80" /> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_061t2y6_di" bpmnElement="ApproveAgreementRuleTask">
<dc:Bounds x="320" y="210" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_1lbkm1m_di" bpmnElement="Gateway_1lbkm1m" isMarkerVisible="true">
<dc:Bounds x="475" y="225" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="468" y="186" width="65" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_12k9z5m_di" bpmnElement="ApproveLoanAgreementServiceTask">
<dc:Bounds x="610" y="210" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1to38de_di" bpmnElement="RejectLoanAgreementServiceTask">
<dc:Bounds x="610" y="320" width="100" height="80" />
<bpmndi:BPMNLabel /> <bpmndi:BPMNLabel />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0xmpbgn_di" bpmnElement="IsApprovedGateway" isMarkerVisible="true"> <bpmndi:BPMNShape id="Event_1e7aj39_di" bpmnElement="LoanAgreementNotApprovedEndEvent">
<dc:Bounds x="485" y="212" width="50" height="50" /> <dc:Bounds x="802" y="342" width="36" height="36" />
<bpmndi:BPMNLabel> <bpmndi:BPMNLabel>
<dc:Bounds x="478" y="182" width="65" height="27" /> <dc:Bounds x="782" y="385" width="77" height="27" />
</bpmndi:BPMNLabel> </bpmndi:BPMNLabel>
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_13yt0mp_di" bpmnElement="ApproveLoanAgreementServiceTask"> <bpmndi:BPMNShape id="Event_0b9mm01_di" bpmnElement="SendCrossSellingEvent">
<dc:Bounds x="600" y="197" width="100" height="80" /> <dc:Bounds x="802" y="232" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="780" y="275" width="82" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BPMNShape_0ya5h8m" bpmnElement="LoanAgreementApprovedEndEvent">
<dc:Bounds x="936" y="232" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="916" y="275" width="77" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Participant_1mw44p0_di" bpmnElement="Participant_1xcghje" isHorizontal="true">
<dc:Bounds x="510" y="80" width="300" height="60" />
<bpmndi:BPMNLabel /> <bpmndi:BPMNLabel />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1uvc9z4_di" bpmnElement="RejectLoanAgreementServiceTask"> <bpmndi:BPMNShape id="BPMNShape_0elm7s8" bpmnElement="Participant_10a5hxf" isHorizontal="true">
<dc:Bounds x="600" y="320" width="100" height="80" /> <dc:Bounds x="510" y="470" width="300" height="60" />
<bpmndi:BPMNLabel /> <bpmndi:BPMNLabel />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_086ty8n_di" bpmnElement="LoanAgreementReciedStartEvent"> <bpmndi:BPMNEdge id="Flow_1a4toos_di" bpmnElement="Flow_1a4toos">
<dc:Bounds x="222" y="219" width="36" height="36" /> <di:waypoint x="660" y="210" />
<bpmndi:BPMNLabel> <di:waypoint x="660" y="140" />
<dc:Bounds x="202" y="262" width="77" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0mxjaa5_di" bpmnElement="LoanAgreementApprovedEndEvent">
<dc:Bounds x="882" y="219" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="862" y="262" width="77" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1hg5yw3_di" bpmnElement="SendCrossSellingEvent">
<dc:Bounds x="772" y="219" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="750" y="262" width="82" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1i3yo9j_di" bpmnElement="LoanAgreementNotApprovedEndEvent">
<dc:Bounds x="772" y="342" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="752" y="385" width="77" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Participant_1s9mw1a_di" bpmnElement="Participant_0y932pu" isHorizontal="true">
<dc:Bounds x="500" y="460" width="300" height="60" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BPMNShape_1ilsqo5" bpmnElement="Participant_0z5fkq5" isHorizontal="true">
<dc:Bounds x="500" y="80" width="300" height="60" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1ytdopc_di" bpmnElement="Flow_1ytdopc">
<di:waypoint x="650" y="400" />
<di:waypoint x="650" y="460" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1ax0a41_di" bpmnElement="Flow_1ax0a41"> <bpmndi:BPMNEdge id="Flow_1sr6eeg_di" bpmnElement="Flow_1sr6eeg">
<di:waypoint x="650" y="197" /> <di:waypoint x="660" y="400" />
<di:waypoint x="650" y="140" /> <di:waypoint x="660" y="470" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram> </bpmndi:BPMNDiagram>

View File

@@ -1,40 +1,23 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement; package de.weinbrecht.luc.bpm.architecture.loan.agreement;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.process.ApproveLoanAgreement; import io.camunda.zeebe.client.ZeebeClient;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.process.RejectionLoanAgreement; import io.camunda.zeebe.client.api.response.PublishMessageResponse;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.process.SendCrossSellingRecommendation; import io.camunda.zeebe.process.test.api.ZeebeTestEngine;
import org.camunda.bpm.dmn.engine.DmnDecisionTableResult; import io.camunda.zeebe.process.test.extension.ZeebeProcessTest;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.test.Deployment;
import org.camunda.bpm.extension.junit5.test.ProcessEngineExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Map; import static de.weinbrecht.luc.bpm.architecture.ProcessTestUtils.*;
import java.util.stream.Stream; import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.*;
import static io.camunda.zeebe.process.test.assertions.BpmnAssert.assertThat;
import static java.util.Collections.singletonMap;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_AGREEMENT_NUMBER; @ZeebeProcessTest
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.assertThat;
import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.decisionService;
import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.execute;
import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.job;
import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.runtimeService;
import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.withVariables;
import static org.camunda.community.mockito.DelegateExpressions.registerJavaDelegateMock;
import static org.camunda.community.mockito.DelegateExpressions.verifyJavaDelegateMock;
@ExtendWith(ProcessEngineExtension.class)
class ProcessTest { class ProcessTest {
public static final String PROCESS_DEFINITION = "Loan_Agreement"; private ZeebeTestEngine engine;
private ZeebeClient client;
private static final String START_EVENT = "LoanAgreementReciedStartEvent"; private static final String START_EVENT = "LoanAgreementReceivedStartEvent";
private static final String APPROVE_RULE_TASK = "ApproveAgreementRuleTask"; private static final String APPROVE_RULE_TASK = "ApproveAgreementRuleTask";
private static final String APPROVE_AGREEMENT_SERVICE_TASK = "ApproveLoanAgreementServiceTask"; private static final String APPROVE_AGREEMENT_SERVICE_TASK = "ApproveLoanAgreementServiceTask";
private static final String SEND_CROSS_SELLING_EVENT = "SendCrossSellingEvent"; private static final String SEND_CROSS_SELLING_EVENT = "SendCrossSellingEvent";
@@ -43,84 +26,46 @@ class ProcessTest {
private static final String REJECT_AGREEMENT_SERVICE_TASK = "RejectLoanAgreementServiceTask"; private static final String REJECT_AGREEMENT_SERVICE_TASK = "RejectLoanAgreementServiceTask";
private static final String NOT_APPROVED_END_EVENT = "LoanAgreementNotApprovedEndEvent"; private static final String NOT_APPROVED_END_EVENT = "LoanAgreementNotApprovedEndEvent";
private static final String DMN_DEFINITION = "approvement-check"; @Test
void testRunsHappyPath() throws Exception {
deployResources(client, "loan_agreement.bpmn", "approve_agreement.dmn");
@BeforeEach final PublishMessageResponse response = sendMessage(engine, client,
void setUp() { LOAN_START_EVENT_MESSAGE_REF, "", singletonMap(LOAN_AGREEMENT_NUMBER, 1L));
registerJavaDelegateMock(ApproveLoanAgreement.class);
registerJavaDelegateMock(RejectionLoanAgreement.class); assertThat(response).extractingProcessInstance()
registerJavaDelegateMock(SendCrossSellingRecommendation.class); .hasPassedElementsInOrder(START_EVENT, APPROVE_RULE_TASK)
.isWaitingAtElements(APPROVE_AGREEMENT_SERVICE_TASK);
completeTaskWithType(engine , client, APPROVE_AGREEMENT_SERVICE_TASK, LOAN_AGREEMENT_TASK);
assertThat(response).extractingProcessInstance()
.hasPassedElement(START_EVENT)
.isWaitingAtElements(SEND_CROSS_SELLING_EVENT);
completeTaskWithType(engine , client, SEND_CROSS_SELLING_EVENT, SEND_CROSS_SELLING_RECOMMENDATION_TASK);
assertThat(response).extractingProcessInstance()
.hasPassedElementsInOrder(SEND_CROSS_SELLING_EVENT, APPROVED_END_EVENT)
.isCompleted();
} }
@Test @Test
@Deployment(resources = { "loan_agreement.bpmn", "approve_agreement.dmn"}) void testRunsExceptionPath() throws Exception {
void shouldExecuteProcess_happy_path() { deployResources(client, "loan_agreement.bpmn", "approve_agreement.dmn");
ProcessInstance processInstance = runtimeService().startProcessInstanceByKey(
PROCESS_DEFINITION,
withVariables(LOAN_AGREEMENT_NUMBER, 1L)
);
assertThat(processInstance).isActive();
assertThat(processInstance).isWaitingAt(START_EVENT); final PublishMessageResponse response = sendMessage(engine, client,
LOAN_START_EVENT_MESSAGE_REF, "", singletonMap(LOAN_AGREEMENT_NUMBER, 6L));
execute(job()); assertThat(response).extractingProcessInstance()
.hasPassedElementsInOrder(START_EVENT, APPROVE_RULE_TASK)
.isWaitingAtElements(REJECT_AGREEMENT_SERVICE_TASK);
assertThat(processInstance) completeTaskWithType(engine , client, REJECT_AGREEMENT_SERVICE_TASK, LOAN_REJECTION_TASK);
.hasPassed(START_EVENT,
APPROVE_RULE_TASK,
APPROVE_AGREEMENT_SERVICE_TASK,
SEND_CROSS_SELLING_EVENT,
APPROVED_END_EVENT);
verifyJavaDelegateMock(SendCrossSellingRecommendation.class).executed(); assertThat(response).extractingProcessInstance()
verifyJavaDelegateMock(ApproveLoanAgreement.class).executed(); .hasPassedElementsInOrder(NOT_APPROVED_END_EVENT)
.hasNotPassedElement(SEND_CROSS_SELLING_EVENT)
assertThat(processInstance).isEnded(); .isCompleted();
}
@Test
@Deployment(resources = { "loan_agreement.bpmn", "approve_agreement.dmn"})
void shouldExecuteProcess_exceptional_path() {
ProcessInstance processInstance = runtimeService().startProcessInstanceByKey(
PROCESS_DEFINITION,
withVariables(LOAN_AGREEMENT_NUMBER, 8L)
);
assertThat(processInstance).isActive();
assertThat(processInstance).isWaitingAt(START_EVENT);
execute(job());
assertThat(processInstance)
.hasPassed(START_EVENT,
APPROVE_RULE_TASK,
REJECT_AGREEMENT_SERVICE_TASK,
NOT_APPROVED_END_EVENT);
verifyJavaDelegateMock(RejectionLoanAgreement.class).executed();
assertThat(processInstance).isEnded();
}
@ParameterizedTest
@MethodSource("provideProcessVariablesForDMN")
@Deployment(resources = "approve_agreement.dmn")
void testApprovementDMN(Long input, boolean expected) {
Map<String, Object> variables = withVariables(LOAN_AGREEMENT_NUMBER, input);
DmnDecisionTableResult tableResult = decisionService().evaluateDecisionTableByKey(DMN_DEFINITION, variables);
assertThat(tableResult.getFirstResult()).contains(entry("approved", expected));
}
private static Stream<Arguments> provideProcessVariablesForDMN() {
return Stream.of(
Arguments.of(1L, true),
Arguments.of(2L, true),
Arguments.of(3L, true),
Arguments.of(4L, true),
Arguments.of(5L, false),
Arguments.of(6L, false)
);
} }
} }

View File

@@ -0,0 +1,130 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.*;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.recipient.MailAddress;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.recipient.Name;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.recipient.Recipient;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in.LoanAgreementCreation;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in.LoanAgreementStatusCommand;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementCommand;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementQuery;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.RecommendationTrigger;
import io.camunda.zeebe.client.ZeebeClient;
import io.camunda.zeebe.process.test.api.ZeebeTestEngine;
import io.camunda.zeebe.spring.test.ZeebeSpringTest;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import java.util.HashMap;
import java.util.Map;
import static de.weinbrecht.luc.bpm.architecture.SpringProcessTestUtils.*;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.*;
import static io.camunda.zeebe.process.test.filters.RecordStream.of;
import static java.time.Duration.ofSeconds;
import static java.util.Collections.singletonMap;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
// https://github.com/camunda-community-hub/camunda-8-examples/blob/main/twitter-review-java-springboot/src/test/java/org/camunda/community/examples/twitter/TestTwitterProcess.java
@SpringBootTest
@ZeebeSpringTest
@Disabled("Flaky if runs together with the non Spring Process Tests")
@MockBean({LoanAgreementCommand.class, LoanAgreementCreation.class})
class SpringProcessTest {
@Autowired
private ZeebeClient zeebe;
@Autowired
private ZeebeTestEngine zeebeTestEngine;
@MockBean
private LoanAgreementStatusCommand loanAgreementStatusCommand;
@MockBean
private RecommendationTrigger recommendationTrigger;
@MockBean
private LoanAgreementQuery loanAgreementQuery;
public static final String PROCESS_DEFINITION = "Loan_Agreement";
private static final String START_EVENT = "LoanAgreementReceivedStartEvent";
private static final String APPROVE_RULE_TASK = "ApproveAgreementRuleTask";
private static final String APPROVE_AGREEMENT_SERVICE_TASK = "ApproveLoanAgreementServiceTask";
private static final String SEND_CROSS_SELLING_EVENT = "SendCrossSellingEvent";
private static final String APPROVED_END_EVENT = "LoanAgreementApprovedEndEvent";
private static final String REJECT_AGREEMENT_SERVICE_TASK = "RejectLoanAgreementServiceTask";
private static final String NOT_APPROVED_END_EVENT = "LoanAgreementNotApprovedEndEvent";
@Test
void testRunsProcessHappyPath() throws Exception {
final LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L);
final CaseId caseId = new CaseId("Test-A-B");
LoanAgreement loanAgreement = new LoanAgreement(
loanAgreementNumber,
new Recipient(
new CustomerNumber("Test-1"),
new Name("Tester"),
new MailAddress("tester@web.io")
),
new Amount(100)
);
when(loanAgreementQuery.loadByNumber(loanAgreementNumber)).thenReturn(loanAgreement);
Map<String, Object> processVariables = new HashMap<>();
processVariables.put(LOAN_AGREEMENT_NUMBER, loanAgreementNumber.getValue());
processVariables.put(BUSINESS_KEY, caseId.getValue());
zeebe.newPublishMessageCommand().messageName(LOAN_START_EVENT_MESSAGE_REF)
.correlationKey("")
.variables(processVariables)
.send()
.join();
hasPassedElement(START_EVENT, of(zeebeTestEngine.getRecordStreamSource()), PROCESS_DEFINITION);
zeebeTestEngine.waitForIdleState(ofSeconds(5));
hasPassedElement(APPROVE_RULE_TASK, of(zeebeTestEngine.getRecordStreamSource()), PROCESS_DEFINITION);
waitForTaskAndComplete(zeebeTestEngine, zeebe, APPROVE_AGREEMENT_SERVICE_TASK, LOAN_AGREEMENT_TASK);
zeebeTestEngine.waitForIdleState(ofSeconds(5));
verify(loanAgreementStatusCommand).accept(loanAgreementNumber);
waitForTaskAndComplete(zeebeTestEngine, zeebe, SEND_CROSS_SELLING_EVENT, SEND_CROSS_SELLING_RECOMMENDATION_TASK);
verify(recommendationTrigger).startLoanAgreement(caseId, loanAgreement);
hasPassedElement(APPROVED_END_EVENT, of(zeebeTestEngine.getRecordStreamSource()), PROCESS_DEFINITION);
assertTrue(isProcessInstanceCompleted(of(zeebeTestEngine.getRecordStreamSource()), PROCESS_DEFINITION));
}
@Test
void testRunsProcessExceptionalPath() throws Exception {
final LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(6L);
zeebe.newPublishMessageCommand().messageName(LOAN_START_EVENT_MESSAGE_REF)
.correlationKey("")
.variables(singletonMap(LOAN_AGREEMENT_NUMBER, loanAgreementNumber.getValue()))
.send()
.join();
zeebeTestEngine.waitForIdleState(ofSeconds(5));
hasPassedElement(START_EVENT, of(zeebeTestEngine.getRecordStreamSource()), PROCESS_DEFINITION);
zeebeTestEngine.waitForIdleState(ofSeconds(5));
hasPassedElement(APPROVE_RULE_TASK, of(zeebeTestEngine.getRecordStreamSource()), PROCESS_DEFINITION);
waitForTaskAndComplete(zeebeTestEngine, zeebe, REJECT_AGREEMENT_SERVICE_TASK, LOAN_REJECTION_TASK);
verify(loanAgreementStatusCommand).reject(loanAgreementNumber);
zeebeTestEngine.waitForIdleState(ofSeconds(5));
hasPassedElement(NOT_APPROVED_END_EVENT, of(zeebeTestEngine.getRecordStreamSource()), PROCESS_DEFINITION);
assertTrue(isProcessInstanceCompleted(of(zeebeTestEngine.getRecordStreamSource()), PROCESS_DEFINITION));
}
}

View File

@@ -2,14 +2,12 @@ package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.process;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber; import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in.LoanAgreementStatusCommand; import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in.LoanAgreementStatusCommand;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_AGREEMENT_NUMBER; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.*;
@MockitoSettings @MockitoSettings
class ApproveLoanAgreementTest { class ApproveLoanAgreementTest {
@@ -22,10 +20,8 @@ class ApproveLoanAgreementTest {
@Test @Test
void should_call_command_and_reject() { void should_call_command_and_reject() {
LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L); LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L);
DelegateExecution delegateExecution = mock(DelegateExecution.class);
when(delegateExecution.getVariable(LOAN_AGREEMENT_NUMBER)).thenReturn(loanAgreementNumber.getValue());
classUnderTest.execute(delegateExecution); classUnderTest.handleJobFoo(loanAgreementNumber.getValue());
verify(loanAgreementStatusCommand).accept(loanAgreementNumber); verify(loanAgreementStatusCommand).accept(loanAgreementNumber);
} }

View File

@@ -2,14 +2,12 @@ package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.process;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber; import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in.LoanAgreementStatusCommand; import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in.LoanAgreementStatusCommand;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_AGREEMENT_NUMBER; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.*;
@MockitoSettings @MockitoSettings
class RejectionLoanAgreementTest { class RejectionLoanAgreementTest {
@@ -23,10 +21,8 @@ class RejectionLoanAgreementTest {
@Test @Test
void should_call_command_and_reject() { void should_call_command_and_reject() {
LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L); LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L);
DelegateExecution delegateExecution = mock(DelegateExecution.class);
when(delegateExecution.getVariable(LOAN_AGREEMENT_NUMBER)).thenReturn(loanAgreementNumber.getValue());
classUnderTest.execute(delegateExecution); classUnderTest.handleJobFoo(loanAgreementNumber.getValue());
verify(loanAgreementStatusCommand).reject(loanAgreementNumber); verify(loanAgreementStatusCommand).reject(loanAgreementNumber);
} }

View File

@@ -4,15 +4,14 @@ import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.CaseId;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement; import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementQuery; import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementQuery;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.RecommendationTrigger; import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.RecommendationTrigger;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_AGREEMENT_NUMBER;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.TestdataGenerator.createLoanAgreementWithNumber; import static de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.TestdataGenerator.createLoanAgreementWithNumber;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@MockitoSettings @MockitoSettings
class SendCrossSellingRecommendationTest { class SendCrossSellingRecommendationTest {
@@ -29,15 +28,11 @@ class SendCrossSellingRecommendationTest {
@Test @Test
void should_load_data_and_start_process_by_message() { void should_load_data_and_start_process_by_message() {
LoanAgreement loanAgreement = createLoanAgreementWithNumber(); LoanAgreement loanAgreement = createLoanAgreementWithNumber();
String caseId = "11"; CaseId caseId = new CaseId("11");
DelegateExecution delegateExecution = mock(DelegateExecution.class);
when(delegateExecution.getBusinessKey()).thenReturn(caseId);
when(delegateExecution.getVariable(LOAN_AGREEMENT_NUMBER))
.thenReturn(loanAgreement.getLoanAgreementNumber().getValue());
when(loanAgreementQuery.loadByNumber(loanAgreement.getLoanAgreementNumber())).thenReturn(loanAgreement); when(loanAgreementQuery.loadByNumber(loanAgreement.getLoanAgreementNumber())).thenReturn(loanAgreement);
classUnderTest.execute(delegateExecution); classUnderTest.handleJobFoo(loanAgreement.getLoanAgreementNumber().getValue(), caseId.getValue());
verify(recommendationTrigger).startLoanAgreement(new CaseId(caseId), loanAgreement); verify(recommendationTrigger).startLoanAgreement(caseId, loanAgreement);
} }
} }

View File

@@ -2,7 +2,7 @@ package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.process;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.CaseId; import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.CaseId;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber; import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
import org.camunda.bpm.engine.RuntimeService; import io.camunda.zeebe.client.ZeebeClient;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
@@ -11,8 +11,8 @@ import org.mockito.junit.jupiter.MockitoSettings;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_AGREEMENT_NUMBER; import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.*;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.common.ProcessConstants.LOAN_START_EVENT_MESSAGE_REF; import static org.mockito.Answers.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@MockitoSettings @MockitoSettings
@@ -21,19 +21,25 @@ class ProcessEngineClientTest {
@InjectMocks @InjectMocks
private ProcessEngineClient classUnderTest; private ProcessEngineClient classUnderTest;
@Mock @Mock(answer = RETURNS_DEEP_STUBS)
private RuntimeService runtimeService; private ZeebeClient client;
private final CaseId caseId = new CaseId("11"); private final CaseId caseId = new CaseId("11");
private final LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L); private final LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L);
@Test @Test
void should_class_runtime_service_to_start() { void should_call_zeebe_to_start_reommendation() {
Map<String, Object> processVariables = new HashMap<>(); Map<String, Object> processVariables = new HashMap<>();
processVariables.put(LOAN_AGREEMENT_NUMBER, loanAgreementNumber.getValue()); processVariables.put(LOAN_AGREEMENT_NUMBER, loanAgreementNumber.getValue());
processVariables.put(BUSINESS_KEY, caseId.getValue());
classUnderTest.startLoanAgreement(caseId, loanAgreementNumber); classUnderTest.startLoanAgreement(caseId, loanAgreementNumber);
verify(runtimeService).startProcessInstanceByMessage(LOAN_START_EVENT_MESSAGE_REF, caseId.getValue(), processVariables); verify(client
.newPublishMessageCommand()
.messageName(LOAN_START_EVENT_MESSAGE_REF)
.correlationKey("")
.variables(processVariables)
).send();
} }
} }

View File

@@ -1,5 +1 @@
spring.datasource.url: jdbc:h2:file:./camunda-h2-database spring.datasource.url: jdbc:h2:file:./camunda-h2-database
camunda.bpm.admin-user:
id: admin
password: pw

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="processEngineConfiguration" class="org.camunda.bpm.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="jdbcUrl" value="jdbc:h2:mem:camunda;DB_CLOSE_DELAY=1000" />
<property name="jdbcDriver" value="org.h2.Driver" />
<property name="jdbcUsername" value="sa" />
<property name="jdbcPassword" value="" />
<!-- Database configurations -->
<property name="databaseSchemaUpdate" value="true" />
<!-- job executor configurations -->
<property name="jobExecutorActivate" value="false" />
<property name="history" value="full" />
</bean>
</beans>

View File

@@ -15,7 +15,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<version.springboot>2.6.4</version.springboot> <version.springboot>2.6.4</version.springboot>
<version.camunda>7.17.0</version.camunda> <version.zeebe>8.0.1</version.zeebe>
<version.junit5>5.8.2</version.junit5> <version.junit5>5.8.2</version.junit5>
<version.lombok>1.18.24</version.lombok> <version.lombok>1.18.24</version.lombok>
<version.domainprimitives>0.1.0</version.domainprimitives> <version.domainprimitives>0.1.0</version.domainprimitives>
@@ -36,9 +36,9 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.camunda.bpm</groupId> <groupId>io.camunda</groupId>
<artifactId>camunda-bom</artifactId> <artifactId>zeebe-bom</artifactId>
<version>${version.camunda}</version> <version>${version.zeebe}</version>
<scope>import</scope> <scope>import</scope>
<type>pom</type> <type>pom</type>
</dependency> </dependency>
@@ -47,18 +47,35 @@
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.camunda.bpm.springboot</groupId> <groupId>io.camunda</groupId>
<artifactId>camunda-bpm-spring-boot-starter-rest</artifactId> <artifactId>spring-zeebe</artifactId>
<version>${version.zeebe}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.camunda.bpm.springboot</groupId> <groupId>io.camunda</groupId>
<artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId> <artifactId>spring-zeebe-starter</artifactId>
<version>${version.zeebe}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.camunda.bpm.springboot</groupId> <groupId>io.camunda</groupId>
<artifactId>camunda-bpm-spring-boot-starter-test</artifactId> <artifactId>spring-zeebe-util</artifactId>
<version>${version.zeebe}</version>
</dependency>
<dependency>
<groupId>io.camunda</groupId>
<artifactId>spring-zeebe-test</artifactId>
<version>${version.zeebe}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.camunda</groupId>
<artifactId>zeebe-process-test-extension</artifactId>
<version>${version.zeebe}</version>
<scope>test</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -113,20 +130,6 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.camunda.bpm.extension</groupId>
<artifactId>camunda-bpm-junit5</artifactId>
<version>${version.bpmAssert}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.camunda.community.mockito</groupId>
<artifactId>camunda-platform-7-mockito</artifactId>
<version>${version.camundaMockito}</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>com.tngtech.archunit</groupId> <groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId> <artifactId>archunit-junit5</artifactId>
@@ -146,6 +149,14 @@
<build> <build>
<finalName>${project.artifactId}</finalName> <finalName>${project.artifactId}</finalName>
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
</testResource>
<testResource>
<directory>${project.basedir}/src/main/resources</directory>
</testResource>
</testResources>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>

View File

@@ -0,0 +1,11 @@
package de.weinbrecht.luc.bpm.architecture.recommendation;
import io.camunda.zeebe.spring.client.EnableZeebeClient;
import io.camunda.zeebe.spring.client.annotation.ZeebeDeployment;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableZeebeClient
@ZeebeDeployment(resources = {"classpath:*.bpmn", "classpath:*.dmn"})
public class ApplicationConfiguration {
}

View File

@@ -5,4 +5,8 @@ public class ProcessConstants {
public static final String CUSTOMER_NUMBER = "customerNumber"; public static final String CUSTOMER_NUMBER = "customerNumber";
public static final String CONTENT_NUMBER = "contentNumber"; public static final String CONTENT_NUMBER = "contentNumber";
public static final String PICK_CONTENT_TASK = "pick-content";
public static final String SEND_RECOMMENDATION_TASK = "send-recommendation";
} }

View File

@@ -0,0 +1,11 @@
package de.weinbrecht.luc.bpm.architecture.recommendation.adapter.in.process;
import io.camunda.zeebe.client.api.response.ActivatedJob;
import static java.lang.String.format;
public class CouldNotCompleteJobException extends RuntimeException {
public CouldNotCompleteJobException(final ActivatedJob job, Throwable cause) {
super(format("Could not complete job %s", job), cause);
}
}

View File

@@ -2,22 +2,35 @@ package de.weinbrecht.luc.bpm.architecture.recommendation.adapter.in.process;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.ContentId; import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.ContentId;
import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.in.RecommendationPicker; import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.in.RecommendationPicker;
import io.camunda.zeebe.client.api.response.ActivatedJob;
import io.camunda.zeebe.client.api.worker.JobClient;
import io.camunda.zeebe.spring.client.annotation.ZeebeWorker;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CONTENT_NUMBER; import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CONTENT_NUMBER;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.PICK_CONTENT_TASK;
@RequiredArgsConstructor @RequiredArgsConstructor
@Component @Component
public class PickContent implements JavaDelegate { public class PickContent {
private final RecommendationPicker recommendationPicker; private final RecommendationPicker recommendationPicker;
@Override @ZeebeWorker(type = PICK_CONTENT_TASK)
public void execute(DelegateExecution execution) { public void handleJobFoo(final JobClient client, final ActivatedJob job) {
ContentId contentId = recommendationPicker.pickContent(); ContentId contentId = recommendationPicker.pickContent();
execution.setVariable(CONTENT_NUMBER, contentId.getValue()); Map<String, Object> variables = new HashMap<>();
variables.put(CONTENT_NUMBER, contentId.getValue());
client.newCompleteCommand(job.getKey())
.variables(variables)
.send()
.exceptionally( throwable -> {
throw new CouldNotCompleteJobException(job, throwable);
});
} }
} }

View File

@@ -7,27 +7,24 @@ import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.C
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.CustomerId; import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.CustomerId;
import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.RecommendationQuery; import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.RecommendationQuery;
import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.SendNotification; import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.SendNotification;
import io.camunda.zeebe.spring.client.annotation.ZeebeVariable;
import io.camunda.zeebe.spring.client.annotation.ZeebeWorker;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.camunda.bpm.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CONTENT_NUMBER; import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.SEND_RECOMMENDATION_TASK;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CUSTOMER_NUMBER;
@RequiredArgsConstructor @RequiredArgsConstructor
@Component @Component
public class SendRecommendation implements JavaDelegate { public class SendRecommendation {
private final SendNotification sendNotification; private final SendNotification sendNotification;
private final RecommendationQuery recommendationQuery; private final RecommendationQuery recommendationQuery;
@Override @ZeebeWorker(type = SEND_RECOMMENDATION_TASK, autoComplete = true)
public void execute(DelegateExecution execution) { public void handleJobFoo(@ZeebeVariable Number contentNumber, @ZeebeVariable String customerNumber) {
Long contentId = (Long) execution.getVariable(CONTENT_NUMBER); Content content = recommendationQuery.findContentById(new ContentId(contentNumber.longValue()));
String customerId = (String) execution.getVariable(CUSTOMER_NUMBER); Customer customer = recommendationQuery.findCustomerById(new CustomerId(customerNumber));
Content content = recommendationQuery.findContentById(new ContentId(contentId));
Customer customer = recommendationQuery.findCustomerById(new CustomerId(customerId));
Recommendation recommendation = new Recommendation(customer, content); Recommendation recommendation = new Recommendation(customer, content);

View File

@@ -2,10 +2,12 @@ package de.weinbrecht.luc.bpm.architecture.recommendation.adapter.out.db.content
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner; import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@RequiredArgsConstructor @RequiredArgsConstructor
@Component @Component
@Profile("!test")
public class ExampleContentRunner implements CommandLineRunner { public class ExampleContentRunner implements CommandLineRunner {
private final ContentCRUDRepository contentCRUDRepository; private final ContentCRUDRepository contentCRUDRepository;

View File

@@ -0,0 +1,7 @@
package de.weinbrecht.luc.bpm.architecture.recommendation.adapter.out.process;
public class CouldNotPublishMessageException extends RuntimeException {
public CouldNotPublishMessageException(Throwable cause) {
super(cause);
}
}

View File

@@ -2,26 +2,32 @@ package de.weinbrecht.luc.bpm.architecture.recommendation.adapter.out.process;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.CustomerId; import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.CustomerId;
import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.StartRecommendation; import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.StartRecommendation;
import io.camunda.zeebe.client.ZeebeClient;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.camunda.bpm.engine.RuntimeService; import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CUSTOMER_NUMBER; import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CUSTOMER_NUMBER;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.START_EVENT_MESSAGE_REF; import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.START_EVENT_MESSAGE_REF;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
@Slf4j
@RequiredArgsConstructor @RequiredArgsConstructor
@Component @Component
public class ProcessEngineClient implements StartRecommendation { public class ProcessEngineClient implements StartRecommendation {
private final RuntimeService runtimeService; private final ZeebeClient client;
@Override @Override
public void start(String caseId, CustomerId customerId) { public void start(String caseId, CustomerId customerId) {
runtimeService.startProcessInstanceByMessage( log.info("Starting new recommendation for loan agreement with case ID {}", caseId);
START_EVENT_MESSAGE_REF, client.newPublishMessageCommand()
caseId, .messageName(START_EVENT_MESSAGE_REF)
singletonMap(CUSTOMER_NUMBER, customerId.getValue()) .correlationKey("")
); .variables(singletonMap(CUSTOMER_NUMBER, customerId.getValue()))
.send()
.exceptionally(throwable -> {
throw new CouldNotPublishMessageException(throwable);
});
} }
} }

View File

@@ -5,12 +5,12 @@ spring:
hibernate: hibernate:
ddl-auto: create-drop ddl-auto: create-drop
camunda.bpm.admin-user: zeebe:
id: admin client:
password: pw cloud:
generic-properties: cluster-id: <your cluster ID>
properties: client-id: <your client ID>
initializeTelemetry: false client-secret: <you secret>
server: server:
port: 8081 port: 8081

View File

@@ -1,65 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:camunda="http://camunda.org/schema/1.0/bpmn" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_02tppq7" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0" modeler:executionPlatform="Camunda Platform" modeler:executionPlatformVersion="7.17.0"> <bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_19vu2ot" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.0.0" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.0.0">
<bpmn:collaboration id="Collaboration_197td5k"> <bpmn:collaboration id="Collaboration_10bbmwl">
<bpmn:participant id="Participant_1y5ozpf" name="Recommendation example" processRef="Cross_Selling_Recommendation" /> <bpmn:participant id="Participant_04vjgyb" name="Recommendation example" processRef="Cross_Selling_Recommendation" />
</bpmn:collaboration> </bpmn:collaboration>
<bpmn:process id="Cross_Selling_Recommendation" name="Cross Selling Recommendation" isExecutable="true"> <bpmn:process id="Cross_Selling_Recommendation" name="Cross Selling Recommendation" isExecutable="true">
<bpmn:serviceTask id="PickContentServiceTask" name="Pick Content" camunda:delegateExpression="${pickContent}">
<bpmn:incoming>Flow_14xjzhy</bpmn:incoming>
<bpmn:outgoing>Flow_0klv1t5</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:startEvent id="CrossSellingStartEvent" name="Cross-Selling potential discovered"> <bpmn:startEvent id="CrossSellingStartEvent" name="Cross-Selling potential discovered">
<bpmn:outgoing>Flow_14xjzhy</bpmn:outgoing> <bpmn:outgoing>Flow_0mgasl2</bpmn:outgoing>
<bpmn:messageEventDefinition id="MessageEventDefinition_1ibjd8g" messageRef="Message_0hftsl2" /> <bpmn:messageEventDefinition id="MessageEventDefinition_0mygnsf" messageRef="Message_2r0s72o" />
</bpmn:startEvent> </bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_14xjzhy" sourceRef="CrossSellingStartEvent" targetRef="PickContentServiceTask" /> <bpmn:endEvent id="CrossSellingRecommendationEndEvent" name="Cross-Selling recommendation sent">
<bpmn:endEvent id="CrossSellingRecommendationEndEvent" name="Cross-Selling  recommendation sent"> <bpmn:incoming>Flow_12yp5p7</bpmn:incoming>
<bpmn:incoming>Flow_1bnyw18</bpmn:incoming>
</bpmn:endEvent> </bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0klv1t5" sourceRef="PickContentServiceTask" targetRef="SendRecommendationServiceTask" /> <bpmn:serviceTask id="SendRecommendationServiceTask" name="Send Recommendation">
<bpmn:sequenceFlow id="Flow_1bnyw18" sourceRef="SendRecommendationServiceTask" targetRef="CrossSellingRecommendationEndEvent" /> <bpmn:extensionElements>
<bpmn:serviceTask id="SendRecommendationServiceTask" name="Send Recommendation" camunda:delegateExpression="${sendRecommendation}"> <zeebe:taskDefinition type="send-recommendation" />
<bpmn:incoming>Flow_0klv1t5</bpmn:incoming> </bpmn:extensionElements>
<bpmn:outgoing>Flow_1bnyw18</bpmn:outgoing> <bpmn:incoming>Flow_1lskvls</bpmn:incoming>
<bpmn:outgoing>Flow_12yp5p7</bpmn:outgoing>
</bpmn:serviceTask> </bpmn:serviceTask>
<bpmn:serviceTask id="PickContentServiceTask" name="Pick Content">
<bpmn:extensionElements>
<zeebe:taskDefinition type="pick-content" />
</bpmn:extensionElements>
<bpmn:incoming>Flow_0mgasl2</bpmn:incoming>
<bpmn:outgoing>Flow_1lskvls</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:sequenceFlow id="Flow_0mgasl2" sourceRef="CrossSellingStartEvent" targetRef="PickContentServiceTask" />
<bpmn:sequenceFlow id="Flow_1lskvls" sourceRef="PickContentServiceTask" targetRef="SendRecommendationServiceTask" />
<bpmn:sequenceFlow id="Flow_12yp5p7" sourceRef="SendRecommendationServiceTask" targetRef="CrossSellingRecommendationEndEvent" />
</bpmn:process> </bpmn:process>
<bpmn:message id="Message_0hftsl2" name="crossSellingPotentialDiscoveredMessage" /> <bpmn:message id="Message_2r0s72o" name="crossSellingPotentialDiscoveredMessage" />
<bpmndi:BPMNDiagram id="BPMNDiagram_1"> <bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_197td5k"> <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_10bbmwl">
<bpmndi:BPMNShape id="Participant_1y5ozpf_di" bpmnElement="Participant_1y5ozpf" isHorizontal="true"> <bpmndi:BPMNShape id="Participant_04vjgyb_di" bpmnElement="Participant_04vjgyb" isHorizontal="true">
<dc:Bounds x="160" y="80" width="610" height="180" /> <dc:Bounds x="160" y="90" width="600" height="190" />
<bpmndi:BPMNLabel /> <bpmndi:BPMNLabel />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1bnyw18_di" bpmnElement="Flow_1bnyw18"> <bpmndi:BPMNEdge id="Flow_12yp5p7_di" bpmnElement="Flow_12yp5p7">
<di:waypoint x="600" y="160" /> <di:waypoint x="600" y="177" />
<di:waypoint x="662" y="160" /> <di:waypoint x="662" y="177" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0klv1t5_di" bpmnElement="Flow_0klv1t5"> <bpmndi:BPMNEdge id="Flow_1lskvls_di" bpmnElement="Flow_1lskvls">
<di:waypoint x="440" y="160" /> <di:waypoint x="440" y="177" />
<di:waypoint x="500" y="160" /> <di:waypoint x="500" y="177" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_14xjzhy_di" bpmnElement="Flow_14xjzhy"> <bpmndi:BPMNEdge id="Flow_0mgasl2_di" bpmnElement="Flow_0mgasl2">
<di:waypoint x="288" y="160" /> <di:waypoint x="285" y="177" />
<di:waypoint x="340" y="160" /> <di:waypoint x="340" y="177" />
</bpmndi:BPMNEdge> </bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_0bsm7s4_di" bpmnElement="PickContentServiceTask"> <bpmndi:BPMNShape id="Event_076x431_di" bpmnElement="CrossSellingStartEvent">
<dc:Bounds x="340" y="120" width="100" height="80" /> <dc:Bounds x="249" y="159" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="234" y="202" width="66" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0hl5l0c_di" bpmnElement="CrossSellingRecommendationEndEvent">
<dc:Bounds x="662" y="159" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="639" y="202" width="82" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1aukzy6_di" bpmnElement="SendRecommendationServiceTask">
<dc:Bounds x="500" y="137" width="100" height="80" />
<bpmndi:BPMNLabel /> <bpmndi:BPMNLabel />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1ct00fd_di" bpmnElement="CrossSellingStartEvent"> <bpmndi:BPMNShape id="Activity_0eljoe1_di" bpmnElement="PickContentServiceTask">
<dc:Bounds x="252" y="142" width="36" height="36" /> <dc:Bounds x="340" y="137" width="100" height="80" />
<bpmndi:BPMNLabel>
<dc:Bounds x="237" y="185" width="66" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_18vk8pm_di" bpmnElement="CrossSellingRecommendationEndEvent">
<dc:Bounds x="662" y="142" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="640" y="185" width="82" height="40" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="BPMNShape_1shn36r" bpmnElement="SendRecommendationServiceTask">
<dc:Bounds x="500" y="120" width="100" height="80" />
<bpmndi:BPMNLabel /> <bpmndi:BPMNLabel />
</bpmndi:BPMNShape> </bpmndi:BPMNShape>
</bpmndi:BPMNPlane> </bpmndi:BPMNPlane>

View File

@@ -1,52 +1,46 @@
package de.weinbrecht.luc.bpm.architecture.recommendation; package de.weinbrecht.luc.bpm.architecture.recommendation;
import de.weinbrecht.luc.bpm.architecture.recommendation.adapter.in.process.PickContent; import io.camunda.zeebe.client.ZeebeClient;
import de.weinbrecht.luc.bpm.architecture.recommendation.adapter.in.process.SendRecommendation; import io.camunda.zeebe.client.api.response.PublishMessageResponse;
import org.camunda.bpm.engine.runtime.ProcessInstance; import io.camunda.zeebe.process.test.api.ZeebeTestEngine;
import org.camunda.bpm.engine.test.Deployment; import io.camunda.zeebe.process.test.extension.ZeebeProcessTest;
import org.camunda.bpm.extension.junit5.test.ProcessEngineExtension;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.assertThat; import static de.weinbrecht.luc.bpm.architecture.ProcessTestUtils.*;
import static org.camunda.bpm.engine.test.assertions.bpmn.BpmnAwareTests.runtimeService; import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.*;
import static org.camunda.community.mockito.DelegateExpressions.registerJavaDelegateMock; import static io.camunda.zeebe.process.test.assertions.BpmnAssert.assertThat;
import static org.camunda.community.mockito.DelegateExpressions.verifyJavaDelegateMock;
@ExtendWith(ProcessEngineExtension.class) @ZeebeProcessTest
class ProcessTest { class ProcessTest {
public static final String PROCESS_DEFINITION = "Cross_Selling_Recommendation"; private ZeebeTestEngine engine;
private ZeebeClient client;
private static final String START_EVENT = "CrossSellingStartEvent"; private static final String START_EVENT = "CrossSellingStartEvent";
private static final String PICK_CONTENT_SERVICE_TASK = "PickContentServiceTask"; private static final String PICK_CONTENT_SERVICE_TASK = "PickContentServiceTask";
private static final String SEND_RECOMMENDATION_SERVICE_TASK = "SendRecommendationServiceTask"; private static final String SEND_RECOMMENDATION_SERVICE_TASK = "SendRecommendationServiceTask";
private static final String END_EVENT = "CrossSellingRecommendationEndEvent"; private static final String END_EVENT = "CrossSellingRecommendationEndEvent";
@BeforeEach
void setUp() {
registerJavaDelegateMock(PickContent.class);
registerJavaDelegateMock(SendRecommendation.class);
}
@Test @Test
@Deployment(resources = "cross_selling_recommendation.bpmn") void testRunsProcess() throws Exception {
void shouldExecuteProcess_happy_path() { deployResource(client, "cross_selling_recommendation.bpmn");
ProcessInstance processInstance = runtimeService().startProcessInstanceByKey(
PROCESS_DEFINITION
);
assertThat(processInstance) final PublishMessageResponse response = sendMessage(engine, client, START_EVENT_MESSAGE_REF, "");
.hasPassedInOrder(
START_EVENT,
PICK_CONTENT_SERVICE_TASK,
SEND_RECOMMENDATION_SERVICE_TASK,
END_EVENT);
verifyJavaDelegateMock(PickContent.class).executed(); assertThat(response).extractingProcessInstance()
verifyJavaDelegateMock(SendRecommendation.class).executed(); .hasPassedElement(START_EVENT)
.isWaitingAtElements(PICK_CONTENT_SERVICE_TASK);
assertThat(processInstance).isEnded(); completeTaskWithType(engine , client, PICK_CONTENT_SERVICE_TASK, PICK_CONTENT_TASK);
assertThat(response).extractingProcessInstance()
.hasPassedElement(PICK_CONTENT_SERVICE_TASK)
.isWaitingAtElements(SEND_RECOMMENDATION_SERVICE_TASK);
completeTaskWithType(engine , client, SEND_RECOMMENDATION_SERVICE_TASK, SEND_RECOMMENDATION_TASK);
assertThat(response).extractingProcessInstance()
.hasPassedElementsInOrder(SEND_RECOMMENDATION_SERVICE_TASK, END_EVENT)
.isCompleted();
} }
} }

View File

@@ -0,0 +1,99 @@
package de.weinbrecht.luc.bpm.architecture.recommendation;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.Content;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.ContentId;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.Description;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.Recommendation;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.Customer;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.CustomerId;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.MailAddress;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.Name;
import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.in.RecommendationCreation;
import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.in.RecommendationPicker;
import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.RecommendationQuery;
import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.SendNotification;
import io.camunda.zeebe.client.ZeebeClient;
import io.camunda.zeebe.process.test.api.ZeebeTestEngine;
import io.camunda.zeebe.spring.test.ZeebeSpringTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.annotation.DirtiesContext;
import static de.weinbrecht.luc.bpm.architecture.SpringProcessTestUtils.isProcessInstanceCompleted;
import static de.weinbrecht.luc.bpm.architecture.SpringProcessTestUtils.waitForTaskAndComplete;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.*;
import static io.camunda.zeebe.process.test.filters.RecordStream.of;
import static java.util.Collections.singletonMap;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_CLASS;
// https://github.com/camunda-community-hub/camunda-8-examples/blob/main/twitter-review-java-springboot/src/test/java/org/camunda/community/examples/twitter/TestTwitterProcess.java
@SpringBootTest
@ZeebeSpringTest
@Disabled("Flaky if runs together with the non Spring Process Tests")
@MockBean(RecommendationCreation.class)
class SpringProcessTest {
@Autowired
private ZeebeClient zeebe;
@Autowired
private ZeebeTestEngine zeebeTestEngine;
@MockBean
private RecommendationPicker recommendationPicker;
@MockBean
private RecommendationQuery recommendationQuery;
@MockBean
private SendNotification sendNotification;
private final ContentId contentId = new ContentId(1L);
private final CustomerId customerId = new CustomerId("Test-11");
private final Recommendation recommendation = new Recommendation(
new Customer(
customerId,
new Name("Tester"),
new MailAddress("tester@ewb.io")
),
new Content(contentId, new Description("Foo"))
);
public static final String PROCESS_DEFINITION = "Cross_Selling_Recommendation";
private static final String PICK_CONTENT_SERVICE_TASK = "PickContentServiceTask";
private static final String SEND_RECOMMENDATION_SERVICE_TASK = "SendRecommendationServiceTask";
@BeforeEach
void setUp() {
when(recommendationPicker.pickContent()).thenReturn(contentId);
when(recommendationQuery.findContentById(contentId)).thenReturn(recommendation.getContent());
when(recommendationQuery.findCustomerById(customerId)).thenReturn(recommendation.getCustomer());
}
@Test
void testRunsProcess() throws Exception {
zeebe.newPublishMessageCommand().messageName(START_EVENT_MESSAGE_REF)
.correlationKey("")
.variables(singletonMap(CUSTOMER_NUMBER, customerId.getValue()))
.send();
waitForTaskAndComplete(zeebeTestEngine, zeebe, PICK_CONTENT_SERVICE_TASK, PICK_CONTENT_TASK, singletonMap(CONTENT_NUMBER, contentId.getValue()));
verify(recommendationPicker).pickContent();
waitForTaskAndComplete(zeebeTestEngine, zeebe, SEND_RECOMMENDATION_SERVICE_TASK, SEND_RECOMMENDATION_TASK);
verify(sendNotification).send(recommendation);
assertTrue(isProcessInstanceCompleted(of(zeebeTestEngine.getRecordStreamSource()), PROCESS_DEFINITION));
}
}

View File

@@ -2,13 +2,18 @@ package de.weinbrecht.luc.bpm.architecture.recommendation.adapter.in.process;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.ContentId; import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.ContentId;
import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.in.RecommendationPicker; import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.in.RecommendationPicker;
import org.camunda.bpm.engine.delegate.DelegateExecution; import io.camunda.zeebe.client.api.response.ActivatedJob;
import io.camunda.zeebe.client.api.worker.JobClient;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import java.util.HashMap;
import java.util.Map;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CONTENT_NUMBER; import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CONTENT_NUMBER;
import static org.mockito.Answers.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@MockitoSettings @MockitoSettings
@@ -24,10 +29,14 @@ class PickContentTest {
void should_call_service_and_set_variable() { void should_call_service_and_set_variable() {
ContentId contentId = new ContentId(1L); ContentId contentId = new ContentId(1L);
when(recommendationPicker.pickContent()).thenReturn(contentId); when(recommendationPicker.pickContent()).thenReturn(contentId);
DelegateExecution delegateExecution = mock(DelegateExecution.class); JobClient client = mock(JobClient.class, RETURNS_DEEP_STUBS);
ActivatedJob job = mock(ActivatedJob.class);
when(job.getKey()).thenReturn(1L);
Map<String, Object> variables = new HashMap<>();
variables.put(CONTENT_NUMBER, contentId.getValue());
classUnderTest.execute(delegateExecution); classUnderTest.handleJobFoo(client, job);
verify(delegateExecution).setVariable(CONTENT_NUMBER, contentId.getValue()); verify(client.newCompleteCommand(job.getKey()).variables(variables)).send();
} }
} }

View File

@@ -10,14 +10,19 @@ import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.M
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.Name; import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.Name;
import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.RecommendationQuery; import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.RecommendationQuery;
import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.SendNotification; import de.weinbrecht.luc.bpm.architecture.recommendation.usecase.out.SendNotification;
import org.camunda.bpm.engine.delegate.DelegateExecution; import io.camunda.zeebe.client.api.response.ActivatedJob;
import io.camunda.zeebe.client.api.worker.JobClient;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import java.util.HashMap;
import java.util.Map;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CONTENT_NUMBER; import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CONTENT_NUMBER;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CUSTOMER_NUMBER; import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CUSTOMER_NUMBER;
import static org.mockito.Answers.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
@MockitoSettings @MockitoSettings
@@ -35,16 +40,13 @@ class SendRecommendationTest {
@Test @Test
void should_load_data_and_call_service_to_send_notification() { void should_load_data_and_call_service_to_send_notification() {
ContentId contentId = new ContentId(1L); ContentId contentId = new ContentId(1L);
CustomerId customerId = new CustomerId("A1"); CustomerId customerNumber = new CustomerId("A1");
DelegateExecution delegateExecution = mock(DelegateExecution.class);
when(delegateExecution.getVariable(CONTENT_NUMBER)).thenReturn(contentId.getValue());
when(delegateExecution.getVariable(CUSTOMER_NUMBER)).thenReturn(customerId.getValue());
Content content = new Content(contentId, new Description("Foo")); Content content = new Content(contentId, new Description("Foo"));
when(recommendationQuery.findContentById(contentId)).thenReturn(content); when(recommendationQuery.findContentById(contentId)).thenReturn(content);
Customer customer = createCustomer(customerId); Customer customer = createCustomer(customerNumber);
when(recommendationQuery.findCustomerById(customerId)).thenReturn(customer); when(recommendationQuery.findCustomerById(customerNumber)).thenReturn(customer);
classUnderTest.execute(delegateExecution); classUnderTest.handleJobFoo(contentId.getValue(), customerNumber.getValue());
verify(sendNotification).send(new Recommendation(customer, content)); verify(sendNotification).send(new Recommendation(customer, content));
} }

View File

@@ -1,7 +1,7 @@
package de.weinbrecht.luc.bpm.architecture.recommendation.adapter.out.process; package de.weinbrecht.luc.bpm.architecture.recommendation.adapter.out.process;
import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.CustomerId; import de.weinbrecht.luc.bpm.architecture.recommendation.domain.model.customer.CustomerId;
import org.camunda.bpm.engine.RuntimeService; import io.camunda.zeebe.client.ZeebeClient;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks; import org.mockito.InjectMocks;
import org.mockito.Mock; import org.mockito.Mock;
@@ -10,6 +10,7 @@ import org.mockito.junit.jupiter.MockitoSettings;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CUSTOMER_NUMBER; import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.CUSTOMER_NUMBER;
import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.START_EVENT_MESSAGE_REF; import static de.weinbrecht.luc.bpm.architecture.recommendation.adapter.common.ProcessConstants.START_EVENT_MESSAGE_REF;
import static java.util.Collections.singletonMap; import static java.util.Collections.singletonMap;
import static org.mockito.Answers.RETURNS_DEEP_STUBS;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
@MockitoSettings @MockitoSettings
@@ -18,8 +19,8 @@ class ProcessEngineClientTest {
@InjectMocks @InjectMocks
private ProcessEngineClient classUnderTest; private ProcessEngineClient classUnderTest;
@Mock @Mock(answer = RETURNS_DEEP_STUBS)
private RuntimeService runtimeService; private ZeebeClient zeebeClient;
@Test @Test
void should_class_runtime_service_to_start() { void should_class_runtime_service_to_start() {
@@ -28,10 +29,11 @@ class ProcessEngineClientTest {
classUnderTest.start(caseId, customerId); classUnderTest.start(caseId, customerId);
verify(runtimeService).startProcessInstanceByMessage( verify(zeebeClient
START_EVENT_MESSAGE_REF, .newPublishMessageCommand()
caseId, .messageName(START_EVENT_MESSAGE_REF)
singletonMap(CUSTOMER_NUMBER, customerId.getValue()) .correlationKey("")
); .variables(singletonMap(CUSTOMER_NUMBER, customerId.getValue()))
).send();
} }
} }

View File

@@ -1,5 +0,0 @@
spring.datasource.url: jdbc:h2:file:./camunda-h2-database
camunda.bpm.admin-user:
id: admin
password: pw

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="processEngineConfiguration" class="org.camunda.bpm.engine.impl.cfg.StandaloneInMemProcessEngineConfiguration">
<property name="jdbcUrl" value="jdbc:h2:mem:camunda;DB_CLOSE_DELAY=1000" />
<property name="jdbcDriver" value="org.h2.Driver" />
<property name="jdbcUsername" value="sa" />
<property name="jdbcPassword" value="" />
<!-- Database configurations -->
<property name="databaseSchemaUpdate" value="true" />
<!-- job executor configurations -->
<property name="jobExecutorActivate" value="false" />
<property name="history" value="full" />
</bean>
</beans>

View File

@@ -14,9 +14,9 @@
<version.junit5>5.8.2</version.junit5> <version.junit5>5.8.2</version.junit5>
<version.bpmAssert>1.1.0</version.bpmAssert> <version.bpmAssert>1.1.0</version.bpmAssert>
<version.camundaMockito>6.17.0</version.camundaMockito>
<version.archunitJunit5>0.23.1</version.archunitJunit5> <version.archunitJunit5>0.23.1</version.archunitJunit5>
<version.surefirePlugin>3.0.0-M6</version.surefirePlugin> <version.surefirePlugin>3.0.0-M6</version.surefirePlugin>
<version.zeebe>8.0.1</version.zeebe>
</properties> </properties>
<dependencies> <dependencies>
@@ -38,17 +38,23 @@
<version>${version.bpmAssert}</version> <version>${version.bpmAssert}</version>
</dependency> </dependency>
<dependency>
<groupId>org.camunda.community.mockito</groupId>
<artifactId>camunda-platform-7-mockito</artifactId>
<version>${version.camundaMockito}</version>
</dependency>
<dependency> <dependency>
<groupId>com.tngtech.archunit</groupId> <groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId> <artifactId>archunit-junit5</artifactId>
<version>${version.archunitJunit5}</version> <version>${version.archunitJunit5}</version>
</dependency> </dependency>
<dependency>
<groupId>io.camunda</groupId>
<artifactId>spring-zeebe-test</artifactId>
<version>${version.zeebe}</version>
</dependency>
<dependency>
<groupId>io.camunda</groupId>
<artifactId>zeebe-process-test-extension</artifactId>
<version>${version.zeebe}</version>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@@ -0,0 +1,110 @@
package de.weinbrecht.luc.bpm.architecture;
import io.camunda.zeebe.client.ZeebeClient;
import io.camunda.zeebe.client.api.command.DeployResourceCommandStep1;
import io.camunda.zeebe.client.api.response.ActivateJobsResponse;
import io.camunda.zeebe.client.api.response.DeploymentEvent;
import io.camunda.zeebe.client.api.response.PublishMessageResponse;
import io.camunda.zeebe.process.test.api.ZeebeTestEngine;
import io.camunda.zeebe.process.test.filters.RecordStream;
import io.camunda.zeebe.protocol.record.Record;
import io.camunda.zeebe.protocol.record.value.JobRecordValue;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import static io.camunda.zeebe.process.test.filters.StreamFilter.jobRecords;
import static io.camunda.zeebe.protocol.record.intent.JobIntent.COMPLETED;
import static io.camunda.zeebe.protocol.record.intent.JobIntent.CREATED;
import static java.lang.String.format;
import static java.time.Duration.ofSeconds;
import static java.util.Collections.emptyMap;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
// https://github.com/camunda/zeebe-process-test/blob/main/qa/abstracts/src/main/java/io/camunda/zeebe/process/test/qa/abstracts/util/Utilities.java
public class ProcessTestUtils {
public static DeploymentEvent deployResource(final ZeebeClient client, final String resource) {
return deployResources(client, resource);
}
public static DeploymentEvent deployResources(
final ZeebeClient client, final String... resources) {
final DeployResourceCommandStep1 commandStep1 = client.newDeployResourceCommand();
DeployResourceCommandStep1.DeployResourceCommandStep2 commandStep2 = null;
for (final String process : resources) {
if (commandStep2 == null) {
commandStep2 = commandStep1.addResourceFromClasspath(process);
} else {
commandStep2 = commandStep2.addResourceFromClasspath(process);
}
}
return commandStep2.send().join();
}
public static PublishMessageResponse sendMessage(
final ZeebeTestEngine engine,
final ZeebeClient client,
final String messageName,
final String correlationKey)
throws InterruptedException, TimeoutException {
return sendMessage(engine, client, messageName, correlationKey, emptyMap());
}
public static PublishMessageResponse sendMessage(
final ZeebeTestEngine engine,
final ZeebeClient client,
final String messageName,
final String correlationKey,
final Map<String, Object> variables)
throws InterruptedException, TimeoutException {
final PublishMessageResponse response =
client
.newPublishMessageCommand()
.messageName(messageName)
.correlationKey(correlationKey)
.variables(variables)
.send()
.join();
engine.waitForIdleState(ofSeconds(1));
return response;
}
public static ActivateJobsResponse activateSingleJob(
final ZeebeClient client, final String jobType) {
return client.newActivateJobsCommand().jobType(jobType).maxJobsToActivate(1).send().join();
}
public static void completeTaskWithType(
final ZeebeTestEngine engine, final ZeebeClient client, final String taskId, final String jobType)
throws InterruptedException, TimeoutException {
final List<Record<JobRecordValue>> records =
jobRecords(RecordStream.of(engine.getRecordStreamSource()))
.withElementId(taskId)
.withIntent(CREATED)
.stream()
.collect(toList());
jobRecords(RecordStream.of(engine.getRecordStreamSource()))
.withElementId(taskId)
.withIntent(COMPLETED)
.stream()
.forEach(record -> records.removeIf(r -> record.getKey() == r.getKey()));
if (!records.isEmpty()) {
final Record<JobRecordValue> lastRecord;
lastRecord = records.get(records.size() - 1);
assertThat(lastRecord.getValue().getType()).isEqualTo(jobType);
client.newCompleteCommand(lastRecord.getKey()).send().join();
} else {
throw new IllegalStateException(
format("Tried to complete task `%s`, but it was not found", taskId));
}
engine.waitForIdleState(ofSeconds(1));
}
}

View File

@@ -0,0 +1,110 @@
package de.weinbrecht.luc.bpm.architecture;
import io.camunda.zeebe.client.ZeebeClient;
import io.camunda.zeebe.client.api.response.ActivatedJob;
import io.camunda.zeebe.process.test.api.ZeebeTestEngine;
import io.camunda.zeebe.process.test.filters.RecordStream;
import io.camunda.zeebe.protocol.record.Record;
import io.camunda.zeebe.protocol.record.value.ProcessInstanceRecordValue;
import org.opentest4j.AssertionFailedError;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeoutException;
import static io.camunda.zeebe.process.test.filters.StreamFilter.processInstance;
import static io.camunda.zeebe.protocol.record.RejectionType.NULL_VAL;
import static io.camunda.zeebe.protocol.record.intent.ProcessInstanceIntent.ELEMENT_COMPLETED;
import static io.camunda.zeebe.protocol.record.value.BpmnElementType.PROCESS;
import static java.time.Duration.ofSeconds;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class SpringProcessTestUtils {
public static ActivatedJob waitForTaskAndComplete(
ZeebeTestEngine zeebeTestEngine,
ZeebeClient zeebe,
String taskId,
String jobType) throws Exception {
int maxRetry = 5;
ActivatedJob taskJob = null;
for (int i = 0; i < maxRetry; i++) {
taskJob = waitAndFetchJobs(zeebeTestEngine, zeebe, jobType);
if (taskJob != null) {
// Make sure it is the right one
assertEquals(taskId, taskJob.getElementId());
zeebe.newCompleteCommand(taskJob.getKey()).send().join();
}
}
return taskJob;
}
public static ActivatedJob waitForTaskAndComplete(
ZeebeTestEngine zeebeTestEngine,
ZeebeClient zeebe,
String taskId,
String jobType,
Map<String, Object> variables) throws Exception {
int maxRetry = 5;
ActivatedJob taskJob = null;
for (int i = 0; i < maxRetry; i++) {
taskJob = waitAndFetchJobs(zeebeTestEngine, zeebe, jobType);
if (taskJob != null) {
// Make sure it is the right one
assertEquals(taskId, taskJob.getElementId());
zeebe.newCompleteCommand(taskJob.getKey()).variables(variables).send().join();
}
}
return taskJob;
}
public static ActivatedJob waitAndFetchJobs(
ZeebeTestEngine zeebeTestEngine,
ZeebeClient zeebe,
String jobType) throws InterruptedException, TimeoutException {
// Let the workflow engine do whatever it needs to do
zeebeTestEngine.waitForIdleState(ofSeconds(5));
// Now get all user tasks
List<ActivatedJob> jobs = zeebe.newActivateJobsCommand().jobType(jobType).maxJobsToActivate(1).send().join().getJobs();
if (jobs.isEmpty()) {
return null;
}
return jobs.get(0);
}
public static boolean isProcessInstanceCompleted(RecordStream recordStream, String bpmnProcessId) {
return processInstance(recordStream).withBpmnProcessId(bpmnProcessId)
.withRejectionType(NULL_VAL)
.withBpmnElementType(PROCESS)
.withIntent(ELEMENT_COMPLETED)
.stream().findFirst().isPresent();
}
public static Record<ProcessInstanceRecordValue> getProcessInstance(RecordStream recordStream, String bpmnProcessId) {
return processInstance(recordStream).withBpmnProcessId(bpmnProcessId)
.withRejectionType(NULL_VAL)
.withBpmnElementType(PROCESS)
.stream()
.findFirst()
.orElseThrow(() ->
new AssertionFailedError("Process Instance not found", bpmnProcessId, null));
}
public static void hasPassedElement(String elementId, RecordStream recordStream, String bpmnProcessId) {
processInstance(recordStream).withBpmnProcessId(bpmnProcessId)
.withRejectionType(NULL_VAL)
.withElementId(elementId)
.stream()
.findFirst()
.orElseThrow(() ->
new AssertionFailedError("Element not found", elementId, null));
}
public static long getProcessInstanceId(RecordStream recordStream, String bpmnProcessId) {
return getProcessInstance(recordStream, bpmnProcessId).getKey();
}
}