diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
new file mode 100644
index 0000000..8453b82
--- /dev/null
+++ b/.github/workflows/maven.yml
@@ -0,0 +1,33 @@
+# This workflow will build a Java project with Maven
+# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
+
+name: Maven Build
+
+on:
+ push:
+ branches: [ main, master ]
+ pull_request:
+ branches: [ main, master ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout Code
+ uses: actions/checkout@v2
+
+ - name: Set up JDK 21
+ uses: actions/setup-java@v1
+ with:
+ java-version: 21
+
+ - name: Check Dependencies
+ run: mvn -B dependency:analysis versions:display-dependency-updates --file pom.xml
+
+ - name: Unit Tests
+ run: mvn -B test --file pom.xml
+
+ - name: Build
+ run: mvn -B package --file pom.xml
\ No newline at end of file
diff --git a/docs/Camunda.adoc b/docs/Camunda.adoc
index 80a8681..4d5827b 100644
--- a/docs/Camunda.adoc
+++ b/docs/Camunda.adoc
@@ -1,5 +1,6 @@
:figure-caption!:
:imagesdir: res
+:toc2:
= Camunda 8
@@ -28,7 +29,7 @@ The clients communicate with the Zeebe cluster using https://grpc.io/[gRPC] thro
The https://docs.camunda.io/docs/components/console/introduction-to-console/[Console] component is used to manage the cluster.
-* In the Self-Managed deployment option, Identity is the component that is in charge of authentication and authorization
+* In the Self-Managed deployment option, https://docs.camunda.io/docs/self-managed/identity/what-is-identity/[Identity] is the component that is in charge of authentication and authorization
It is based on https://www.keycloak.org/[Keycloak].
@@ -49,12 +50,26 @@ It is based on https://www.keycloak.org/[Keycloak].
=== Development
* Use the latest https://spring.io/projects/spring-boot[Spring Boot] version: `3.2.X`
+
* Use the latest Java version: https://openjdk.org/projects/jdk/21/[JDK 21]
+
* Use https://github.com/camunda-community-hub/spring-zeebe[Spring Zeebe] to communicate with Zeebe from Spring Boot
+
* The worker's client credentials should be limited to Zeebe and Tasklist
Access to Operate and Optimize is not needed for the workers.
+* The `handlers` package responsibilities are:
+
+1. extract data from the `ActivatedJob` variables,
+2. call the service,
+3. push data back to the `ActivatedJob` variables,
+4. inform the engine of the completion status
+
+* The `services` package's sole responsibility is to communicate with our APIs
+
+It should therefore have no dependency on any Camunda classes.
+
=== Deployment
=== Modeler
@@ -67,14 +82,64 @@ In particular, use Camunda's built-in https://docs.camunda.io/docs/components/be
* Follow Camunda's https://docs.camunda.io/docs/components/best-practices/modeling/naming-technically-relevant-ids/#using-naming-conventions-for-bpmn-ids[best practices] regarding ID names
+This is very important because these names are needed in the tests and are logged by the engine and therefore needed for troubleshooting.
+
* Follow Camunda's https://docs.camunda.io/docs/components/best-practices/modeling/naming-bpmn-elements/[best practices] regarding BPMN element names
+This helps in communicating with other stakeholders.
+
* Use camel case for service tasks' task definition
-* Use camel case for
=== Tasklist
-* As mentioned in https://academy.camunda.com/c8-technical-overview[Camunda 8 - Technical Overview], we should use the Tasklist https://docs.camunda.io/docs/apis-tools/tasklist-api-rest/tasklist-api-rest-overview/[REST API] over the https://docs.camunda.io/docs/apis-tools/tasklist-api/tasklist-api-overview/[GraphQL API] since the latter will be deprecated soon
+* Use the Tasklist https://docs.camunda.io/docs/apis-tools/tasklist-api-rest/tasklist-api-rest-overview/[REST API] over the https://docs.camunda.io/docs/apis-tools/tasklist-api/tasklist-api-overview/[GraphQL API]
+
+The GraphQL API will be deprecated soon as mentioned in https://academy.camunda.com/c8-technical-overview[Camunda 8 - Technical Overview].
+
+=== Testing
+
+* Run an embedded Zeebe test engine using the `zeebe-process-test-extension` dependency footnote:[as opposed to a test container using the `zeebe-process-test-extension-testcontainer` dependency]
+
+* Use the Spring test dependency https://github.com/camunda-community-hub/spring-zeebe#writing-test-cases[`spring-zeebe-test`] which is a wrapper around `zeebe-process-test-extension`
+
+* Use the assertions documented in https://docs.camunda.io/docs/apis-tools/java-client/zeebe-process-test/#assertions[Zeebe Process Test]
+
+* Every path must be tested and in particular every FEEL expression
+
+* For system tests, mock external APIs using https://wiremock.org[WireMock] instead of mocking the services using https://github.com/mockito/mockito[Mockito]
+
+* For system tests, start the process from the start and avoid using the `startBeforeElement` API
+
+* For unit tests, use https://github.com/mockito/mockito[Mockito] to mock the services and start the process just before the class under test is called using the `startBeforeElement` API
+
+* Don't forget to wait in the tests using the `ZeebeTestEngine` wait methods: `waitForIdleState` & `waitForBusyState`
+
+If you don't do that and use assertions about BPMN elements that are after the service or user tasks, the assertions will fail because they will be evaluated before the engine has time to progress in the flow.
+
+== Questions
+
+=== Testing
+
+* How can we avoid redeploying the BPMN diagram before each test ?
+
+[source, java]
+----
+@ZeebeProcessTest
+public class ProcessTest {
+ private ZeebeClient client;
+
+ @BeforeEach
+ public void setup() {
+ DeploymentEvent deploymentEvent = client.newDeployResourceCommand()
+ .addResourceFromClasspath("process.bpmn")
+ .send()
+ .join();
+ }
+}
+----
+
+Even though there is no need to redeploy the same BPMN diagram before each test, we can't use the `BeforeAll` annotation because it forces us to declare the `ZeebeClient` as `static`. +
+As a consequence, the `ZeebeClient` instance is not injected correctly when using the `ZeebeProcessTest` annotation
== Resources
diff --git a/service-orchestration/pom.xml b/java/pom.xml
similarity index 78%
rename from service-orchestration/pom.xml
rename to java/pom.xml
index 258591f..1d09abd 100644
--- a/service-orchestration/pom.xml
+++ b/java/pom.xml
@@ -9,10 +9,15 @@
0.0.1-SNAPSHOT
- camunda-service-orchestration
+ camunda-java
0.0.1-SNAPSHOT
- service-orchestration
- jar
+ java
+ pom
+
+
+ service-orchestration
+ testing
+
@@ -21,4 +26,5 @@
${camunda.version}
+
diff --git a/service-orchestration/README.md b/java/service-orchestration/README.md
similarity index 100%
rename from service-orchestration/README.md
rename to java/service-orchestration/README.md
diff --git a/java/service-orchestration/pom.xml b/java/service-orchestration/pom.xml
new file mode 100644
index 0000000..d734701
--- /dev/null
+++ b/java/service-orchestration/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+
+
+ com.micasa.tutorial
+ camunda-java
+ 0.0.1-SNAPSHOT
+
+
+ camunda-java-service-orchestration
+ 0.0.1-SNAPSHOT
+ service-orchestration
+ jar
+
+
diff --git a/service-orchestration/src/main/java/com/micasa/tutorial/PaymentApplication.java b/java/service-orchestration/src/main/java/com/micasa/tutorial/PaymentApplication.java
similarity index 74%
rename from service-orchestration/src/main/java/com/micasa/tutorial/PaymentApplication.java
rename to java/service-orchestration/src/main/java/com/micasa/tutorial/PaymentApplication.java
index 0c9d828..d5470d8 100644
--- a/service-orchestration/src/main/java/com/micasa/tutorial/PaymentApplication.java
+++ b/java/service-orchestration/src/main/java/com/micasa/tutorial/PaymentApplication.java
@@ -1,25 +1,22 @@
package com.micasa.tutorial;
-import com.micasa.tutorial.handler.CreditCardServiceHandler;
+import com.micasa.tutorial.handler.CreditCardChargingHandler;
import io.camunda.zeebe.client.ZeebeClient;
-import io.camunda.zeebe.client.impl.oauth.OAuthCredentialsProvider;
import io.camunda.zeebe.client.impl.oauth.OAuthCredentialsProviderBuilder;
import java.time.Duration;
import java.util.Map;
public class PaymentApplication {
+
private static final String ZEEBE_ADDRESS = "4784612d-1495-4f2a-951d-049c8b985f06.ont-1.zeebe.camunda.io:443";
private static final String ZEEBE_CLIENT_ID = "jMNcIFp.GJ1-ZYK~Av-8zLHM7xmYCTLs";
private static final String ZEEBE_CLIENT_SECRET = "dbz--V.YX9JzRzF_44iL9DNz.h0_1S-qpPbl~2xYt6f0ua5h3CWQ2wVulQ68b-Rm";
private static final String ZEEBE_AUTHORIZATION_SERVER_URL = "https://login.cloud.camunda.io/oauth/token";
private static final String ZEEBE_TOKEN_AUDIENCE = "zeebe.camunda.io";
- private static final String CAMUNDA_CLUSTER_ID = "4784612d-1495-4f2a-951d-049c8b985f06";
- private static final String CAMUNDA_CLUSTER_REGION = "ont-1";
- private static final String CAMUNDA_CREDENTIALS_SCOPES = "Zeebe";
- private static final String CAMUNDA_OAUTH_URL = "https://login.cloud.camunda.io/oauth/token";
public static void main(String[] args) {
+
// Needed for the client to authenticate with the cluster
final var credentialsProvider = new OAuthCredentialsProviderBuilder()
.authorizationServerUrl(ZEEBE_AUTHORIZATION_SERVER_URL)
@@ -34,8 +31,8 @@ public static void main(String[] args) {
.build()) {
var variables = Map.of(
- "reference", "C8_12345",
- "amount", Double.valueOf(100.00),
+ "orderReference", "C8_12345",
+ "orderAmount", Double.valueOf(100.00),
"cardNumber", "1234567812345678",
"cardExpiry", "12/2023",
"cardCVC", "123"
@@ -49,18 +46,20 @@ public static void main(String[] args) {
.send()
.join();
- // Register this job worker to handle 'chardCreditCard' jobs ( all jobs of this type will be assigned to this worker )
+ // Register & start this job worker to handle 'chardCreditCard' jobs ( all jobs of this type will be assigned to this worker )
var creditCardWorker = client.newWorker()
.jobType("chargeCreditCard")
- .handler(new CreditCardServiceHandler()) // Uses the handler to process and complete the job
+ .handler(new CreditCardChargingHandler()) // Uses the handler to process and complete the job
.timeout(Duration.ofSeconds(10).toMillis()) // if the job is not completed quick enough, the worker releases the job which can then be picked up by another worker
.open();
Thread.sleep(10000);
+ creditCardWorker.close();
+
} catch (Exception e) {
e.printStackTrace();
}
}
-}
+}
\ No newline at end of file
diff --git a/java/service-orchestration/src/main/java/com/micasa/tutorial/handler/CreditCardChargingHandler.java b/java/service-orchestration/src/main/java/com/micasa/tutorial/handler/CreditCardChargingHandler.java
new file mode 100644
index 0000000..46dea46
--- /dev/null
+++ b/java/service-orchestration/src/main/java/com/micasa/tutorial/handler/CreditCardChargingHandler.java
@@ -0,0 +1,44 @@
+package com.micasa.tutorial.handler;
+
+import com.micasa.tutorial.service.CreditCard;
+import com.micasa.tutorial.service.CreditCardService;
+import io.camunda.zeebe.client.api.response.ActivatedJob;
+import io.camunda.zeebe.client.api.worker.JobClient;
+import io.camunda.zeebe.client.api.worker.JobHandler;
+
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+
+public class CreditCardChargingHandler implements JobHandler {
+
+ private final CreditCardService creditCardService;
+
+ public CreditCardChargingHandler(CreditCardService creditCardService) {
+ this.creditCardService = creditCardService;
+ }
+
+ public CreditCardChargingHandler() {
+ this(new CreditCardService());
+ }
+
+ @Override
+ public void handle(JobClient client, ActivatedJob job) throws Exception {
+ var reference = (String) job.getVariable("orderReference");
+ var amount = (Double) job.getVariable("orderAmount");
+ var creditCard = new CreditCard(
+ (String) job.getVariable("cardNumber"),
+ YearMonth.parse((String) job.getVariable("cardExpiry"), DateTimeFormatter.ofPattern("MM/yyyy")),
+ (String) job.getVariable("cardCVC")
+ );
+
+ var confirmationNumber = creditCardService.chargeCreditCard(reference, amount, creditCard);
+
+ // Inform Zeebe that the job was completed successfully and pass along the confirmation number
+ var outputVariables = Map.of("confirmation", confirmationNumber);
+ client.newCompleteCommand(job.getKey())
+ .variables(outputVariables)
+ .send()
+ .join();
+ }
+}
diff --git a/service-orchestration/src/main/java/com/micasa/tutorial/service/CreditCard.java b/java/service-orchestration/src/main/java/com/micasa/tutorial/service/CreditCard.java
similarity index 100%
rename from service-orchestration/src/main/java/com/micasa/tutorial/service/CreditCard.java
rename to java/service-orchestration/src/main/java/com/micasa/tutorial/service/CreditCard.java
diff --git a/service-orchestration/src/main/java/com/micasa/tutorial/service/CreditCardServiceMock.java b/java/service-orchestration/src/main/java/com/micasa/tutorial/service/CreditCardService.java
similarity index 91%
rename from service-orchestration/src/main/java/com/micasa/tutorial/service/CreditCardServiceMock.java
rename to java/service-orchestration/src/main/java/com/micasa/tutorial/service/CreditCardService.java
index f2882ac..e67e965 100644
--- a/service-orchestration/src/main/java/com/micasa/tutorial/service/CreditCardServiceMock.java
+++ b/java/service-orchestration/src/main/java/com/micasa/tutorial/service/CreditCardService.java
@@ -4,10 +4,11 @@
import static java.lang.StringTemplate.STR;
-public class CreditCardServiceMock {
+public class CreditCardService {
public String chargeCreditCard(String transactionNumber, double amount, CreditCard creditCard) {
System.out.println(STR."Charging \{ amount } to credit card \{ creditCard} for transaction \{ transactionNumber }");
return UUID.randomUUID().toString();
}
+
}
diff --git a/service-orchestration/src/main/resources/paymentProcess.bpmn b/java/service-orchestration/src/main/resources/paymentProcess.bpmn
similarity index 100%
rename from service-orchestration/src/main/resources/paymentProcess.bpmn
rename to java/service-orchestration/src/main/resources/paymentProcess.bpmn
diff --git a/java/testing/README.md b/java/testing/README.md
new file mode 100644
index 0000000..0d5542f
--- /dev/null
+++ b/java/testing/README.md
@@ -0,0 +1,3 @@
+# Service Orchestration
+
+This is the implementation for [Camunda 8 - Testing Processes](https://academy.camunda.com/c8-testing-processes).
\ No newline at end of file
diff --git a/java/testing/pom.xml b/java/testing/pom.xml
new file mode 100644
index 0000000..f381ee3
--- /dev/null
+++ b/java/testing/pom.xml
@@ -0,0 +1,26 @@
+
+
+ 4.0.0
+
+
+ com.micasa.tutorial
+ camunda-java
+ 0.0.1-SNAPSHOT
+
+
+ camunda-java-testing
+ 0.0.1-SNAPSHOT
+ testing
+ jar
+
+
+
+ io.camunda
+ zeebe-process-test-extension
+ ${camunda.version}
+ test
+
+
+
+
diff --git a/java/testing/src/main/java/com/micasa/tutorial/PaymentApplication.java b/java/testing/src/main/java/com/micasa/tutorial/PaymentApplication.java
new file mode 100644
index 0000000..8a3b244
--- /dev/null
+++ b/java/testing/src/main/java/com/micasa/tutorial/PaymentApplication.java
@@ -0,0 +1,65 @@
+package com.micasa.tutorial;
+
+import java.time.Duration;
+import java.util.Scanner;
+
+import com.micasa.tutorial.handlers.CreditCardChargingHandler;
+import com.micasa.tutorial.handlers.CreditDeductionHandler;
+
+import io.camunda.zeebe.client.ZeebeClient;
+import io.camunda.zeebe.client.api.worker.JobWorker;
+import io.camunda.zeebe.client.impl.oauth.OAuthCredentialsProviderBuilder;
+
+public class PaymentApplication {
+
+ private static final String ZEEBE_ADDRESS = "4784612d-1495-4f2a-951d-049c8b985f06.ont-1.zeebe.camunda.io:443";
+ private static final String ZEEBE_CLIENT_ID = "jMNcIFp.GJ1-ZYK~Av-8zLHM7xmYCTLs";
+ private static final String ZEEBE_CLIENT_SECRET = "dbz--V.YX9JzRzF_44iL9DNz.h0_1S-qpPbl~2xYt6f0ua5h3CWQ2wVulQ68b-Rm";
+ private static final String ZEEBE_AUTHORIZATION_SERVER_URL = "https://login.cloud.camunda.io/oauth/token";
+ private static final String ZEEBE_TOKEN_AUDIENCE = "zeebe.camunda.io";
+
+ private static final int WORKER_TIMEOUT = 10;
+
+ public static void main(String[] args) {
+
+ // Needed for the client to authenticate with the cluster
+ final var credentialsProvider = new OAuthCredentialsProviderBuilder()
+ .authorizationServerUrl(ZEEBE_AUTHORIZATION_SERVER_URL)
+ .audience(ZEEBE_TOKEN_AUDIENCE)
+ .clientId(ZEEBE_CLIENT_ID)
+ .clientSecret(ZEEBE_CLIENT_SECRET)
+ .build();
+
+ try (final ZeebeClient client = ZeebeClient.newClientBuilder()
+ .gatewayAddress(ZEEBE_ADDRESS)
+ .credentialsProvider(credentialsProvider)
+ .build()) {
+
+ // Start the Credit Deduction Worker
+ final JobWorker creditDeductionWorker = client.newWorker()
+ .jobType("credit-deduction")
+ .handler(new CreditDeductionHandler())
+ .timeout(Duration.ofSeconds(WORKER_TIMEOUT).toMillis())
+ .open();
+
+ // Start the Credit Deduction Worker
+ final JobWorker creditCardChargingWorker = client.newWorker()
+ .jobType("credit-card-charging")
+ .handler(new CreditCardChargingHandler())
+ .timeout(Duration.ofSeconds(WORKER_TIMEOUT).toMillis())
+ .open();
+
+ System.out.println("Type anything to exit");
+ Scanner sc = new Scanner(System.in);
+ sc.next();
+ sc.close();
+
+ creditDeductionWorker.close();
+ creditCardChargingWorker.close();
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/java/testing/src/main/java/com/micasa/tutorial/exceptions/InvalidCreditCardException.java b/java/testing/src/main/java/com/micasa/tutorial/exceptions/InvalidCreditCardException.java
new file mode 100644
index 0000000..f197ea2
--- /dev/null
+++ b/java/testing/src/main/java/com/micasa/tutorial/exceptions/InvalidCreditCardException.java
@@ -0,0 +1,13 @@
+package com.micasa.tutorial.exceptions;
+
+public class InvalidCreditCardException extends Exception {
+
+ public InvalidCreditCardException() {
+ this("Invalid credit card");
+ }
+
+ public InvalidCreditCardException(String message) {
+ super(message);
+ }
+
+}
\ No newline at end of file
diff --git a/java/testing/src/main/java/com/micasa/tutorial/exceptions/MissingVariablesException.java b/java/testing/src/main/java/com/micasa/tutorial/exceptions/MissingVariablesException.java
new file mode 100644
index 0000000..468ed6b
--- /dev/null
+++ b/java/testing/src/main/java/com/micasa/tutorial/exceptions/MissingVariablesException.java
@@ -0,0 +1,9 @@
+package com.micasa.tutorial.exceptions;
+
+public class MissingVariablesException extends RuntimeException {
+
+ public MissingVariablesException(String variables) {
+ super("The following process variables are necessary to complete the job: " + variables);
+ }
+
+}
diff --git a/java/testing/src/main/java/com/micasa/tutorial/handlers/CreditCardChargingHandler.java b/java/testing/src/main/java/com/micasa/tutorial/handlers/CreditCardChargingHandler.java
new file mode 100644
index 0000000..1912196
--- /dev/null
+++ b/java/testing/src/main/java/com/micasa/tutorial/handlers/CreditCardChargingHandler.java
@@ -0,0 +1,59 @@
+package com.micasa.tutorial.handlers;
+
+import com.micasa.tutorial.exceptions.InvalidCreditCardException;
+import com.micasa.tutorial.services.CreditCard;
+import com.micasa.tutorial.services.CreditCardService;
+import io.camunda.zeebe.client.api.response.ActivatedJob;
+import io.camunda.zeebe.client.api.worker.JobClient;
+import io.camunda.zeebe.client.api.worker.JobHandler;
+
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+
+public class CreditCardChargingHandler implements JobHandler {
+
+ private final CreditCardService creditCardService;
+
+ public CreditCardChargingHandler(CreditCardService creditCardService) {
+ this.creditCardService = creditCardService;
+ }
+
+ public CreditCardChargingHandler() {
+ this(new CreditCardService());
+ }
+
+ @Override
+ public void handle(JobClient client, ActivatedJob job) {
+ var reference = (String) job.getVariable("orderReference");
+ var amount = (Double) job.getVariable("orderAmount");
+ var creditCard = new CreditCard(
+ (String) job.getVariable("cardNumber"),
+ YearMonth.parse((String) job.getVariable("cardExpiry"), DateTimeFormatter.ofPattern("MM/yyyy")),
+ (String) job.getVariable("cardCVC")
+ );
+
+ try {
+
+ var confirmationNumber = creditCardService.chargeCreditCard(reference, amount, creditCard);
+
+ var outputVariables = Map.of("confirmation", confirmationNumber);
+ client.newCompleteCommand(job.getKey())
+ .variables(outputVariables)
+ .send()
+ .exceptionally(throwable -> {
+ throw new RuntimeException("Could not complete job " + job, throwable);
+ });
+
+ } catch (InvalidCreditCardException e) {
+ client.newFailCommand(job)
+ .retries(0)
+ .errorMessage(e.getMessage())
+ .send()
+ .exceptionally(throwable -> {
+ throw new RuntimeException("Could not fail job " + job, throwable);
+ });
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/java/testing/src/main/java/com/micasa/tutorial/handlers/CreditDeductionHandler.java b/java/testing/src/main/java/com/micasa/tutorial/handlers/CreditDeductionHandler.java
new file mode 100644
index 0000000..5379454
--- /dev/null
+++ b/java/testing/src/main/java/com/micasa/tutorial/handlers/CreditDeductionHandler.java
@@ -0,0 +1,38 @@
+package com.micasa.tutorial.handlers;
+
+import com.micasa.tutorial.services.CustomerService;
+import io.camunda.zeebe.client.api.response.ActivatedJob;
+import io.camunda.zeebe.client.api.worker.JobClient;
+import java.util.Map;
+
+import io.camunda.zeebe.client.api.worker.JobHandler;
+
+public class CreditDeductionHandler implements JobHandler {
+
+ private final CustomerService customerService;
+
+ public CreditDeductionHandler(CustomerService customerService) {
+ this.customerService = customerService;
+ }
+
+ public CreditDeductionHandler() {
+ this(new CustomerService());
+ }
+
+ @Override
+ public void handle(JobClient client, ActivatedJob job) {
+ var customerCredit = (double) job.getVariable("customerCredit");
+ var amount = (double) job.getVariable("orderAmount");
+
+ var openAmount = customerService.deductCredit(customerCredit, amount);
+
+ job.getVariablesAsMap().put("openAmount", openAmount);
+
+ client.newCompleteCommand(job)
+ .variables(job.getVariablesAsMap())
+ .send()
+ .exceptionally(throwable -> {
+ throw new RuntimeException("Could not complete job " + job, throwable);
+ });
+ }
+}
diff --git a/java/testing/src/main/java/com/micasa/tutorial/services/CreditCard.java b/java/testing/src/main/java/com/micasa/tutorial/services/CreditCard.java
new file mode 100644
index 0000000..7b22cc3
--- /dev/null
+++ b/java/testing/src/main/java/com/micasa/tutorial/services/CreditCard.java
@@ -0,0 +1,6 @@
+package com.micasa.tutorial.services;
+
+import java.time.YearMonth;
+
+public record CreditCard(String cardNumber, YearMonth expiryDate, String CVC) {
+}
diff --git a/java/testing/src/main/java/com/micasa/tutorial/services/CreditCardService.java b/java/testing/src/main/java/com/micasa/tutorial/services/CreditCardService.java
new file mode 100644
index 0000000..877fbb2
--- /dev/null
+++ b/java/testing/src/main/java/com/micasa/tutorial/services/CreditCardService.java
@@ -0,0 +1,19 @@
+package com.micasa.tutorial.services;
+
+import com.micasa.tutorial.exceptions.InvalidCreditCardException;
+
+import java.time.YearMonth;
+import java.util.UUID;
+
+public class CreditCardService {
+
+ public String chargeCreditCard(String transactionNumber, double amount, CreditCard creditCard) throws InvalidCreditCardException {
+ System.out.println(STR. "Charging \{ amount } to credit card \{ creditCard } for transaction \{ transactionNumber }" );
+ if (creditCard.expiryDate().isBefore(YearMonth.now())) {
+ System.out.println("The credit card's expiry date is invalid: " + creditCard.expiryDate());
+ throw new InvalidCreditCardException();
+ }
+ return UUID.randomUUID().toString();
+ }
+
+}
\ No newline at end of file
diff --git a/java/testing/src/main/java/com/micasa/tutorial/services/CustomerService.java b/java/testing/src/main/java/com/micasa/tutorial/services/CustomerService.java
new file mode 100644
index 0000000..f836e7c
--- /dev/null
+++ b/java/testing/src/main/java/com/micasa/tutorial/services/CustomerService.java
@@ -0,0 +1,11 @@
+package com.micasa.tutorial.services;
+
+
+public class CustomerService {
+
+ public double deductCredit(double customerCredit, double amount) {
+ System.out.println(STR. "Deducting \{ amount } from available credit \{ customerCredit }" );
+ return customerCredit > amount ? 0.0 : amount - customerCredit;
+ }
+
+}
diff --git a/java/testing/src/main/resources/payment.bpmn b/java/testing/src/main/resources/payment.bpmn
new file mode 100644
index 0000000..0ee9d25
--- /dev/null
+++ b/java/testing/src/main/resources/payment.bpmn
@@ -0,0 +1,158 @@
+
+
+
+
+ {
+ "components": [
+ {
+ "label": "Credit Card Number",
+ "type": "textfield",
+ "id": "Field_0hkfsm7",
+ "key": "cardNumber"
+ },
+ {
+ "label": "CVC",
+ "type": "textfield",
+ "id": "Field_0xxkod9",
+ "key": "cardCVC"
+ },
+ {
+ "label": "Expiry Date",
+ "type": "textfield",
+ "id": "Field_1v2cack",
+ "key": "cardExpiry"
+ }
+ ],
+ "type": "default",
+ "id": "Form_1xosln6",
+ "executionPlatform": "Camunda Cloud",
+ "executionPlatformVersion": "8.1.0",
+ "exporter": {
+ "name": "Camunda Modeler",
+ "version": "5.7.0"
+ },
+ "schemaVersion": 6
+}
+
+
+
+
+
+ Flow_00blf5e
+ Flow_0d7r970
+
+
+
+
+
+ Flow_05xlz7m
+ Flow_0am0w3r
+
+
+ Flow_0am0w3r
+ YesFlow
+ Flow_1kgy9d6
+
+
+
+
+
+
+ Flow_0d7r970
+ YesFlow
+ NoFlow
+
+
+ =openAmount = 0
+
+
+ =openAmount > 0
+
+
+ Flow_00blf5e
+
+
+ Flow_1kgy9d6
+
+
+
+
+
+ NoFlow
+ Flow_05xlz7m
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/java/testing/src/test/java/com/micasa/tutorial/PaymentProcessTest.java b/java/testing/src/test/java/com/micasa/tutorial/PaymentProcessTest.java
new file mode 100644
index 0000000..4fe0a19
--- /dev/null
+++ b/java/testing/src/test/java/com/micasa/tutorial/PaymentProcessTest.java
@@ -0,0 +1,112 @@
+package com.micasa.tutorial;
+
+import com.micasa.tutorial.handlers.CreditCardChargingHandler;
+import com.micasa.tutorial.handlers.CreditDeductionHandler;
+import io.camunda.zeebe.client.ZeebeClient;
+import io.camunda.zeebe.client.api.response.DeploymentEvent;
+import io.camunda.zeebe.client.api.response.ProcessInstanceEvent;
+import io.camunda.zeebe.process.test.api.ZeebeTestEngine;
+import io.camunda.zeebe.process.test.extension.ZeebeProcessTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.util.Map;
+
+import static com.micasa.tutorial.Utils.*;
+import static io.camunda.zeebe.process.test.assertions.BpmnAssert.assertThat;
+
+@ZeebeProcessTest
+@DisplayName("Payment Process Test")
+public class PaymentProcessTest {
+
+ private ZeebeTestEngine engine;
+ private ZeebeClient client;
+
+ @BeforeEach
+ public void setup() {
+ DeploymentEvent deploymentEvent = client.newDeployResourceCommand()
+ .addResourceFromClasspath("payment.bpmn")
+ .send()
+ .join();
+
+ assertThat(deploymentEvent).containsProcessesByBpmnProcessId("PaymentProcess");
+ }
+
+ @Test
+ @DisplayName("Use customer credit")
+ public void payFromCredit() throws Exception {
+ var variables = Map.of(
+ "orderAmount", 42.0,
+ "customerCredit", 50.0
+ );
+
+ ProcessInstanceEvent processInstance = startProcess(client, "PaymentProcess", variables);
+ completeServiceTask(client, "credit-deduction", new CreditDeductionHandler());
+
+ // Wait for the engine to progress through the flow
+ engine.waitForIdleState(Duration.ofSeconds(1));
+
+ assertThat(processInstance)
+ .hasVariableWithValue("openAmount", 0.0)
+ .hasNotPassedElement("Task_ChargeCreditCard")
+ .hasPassedElement("EndEvent_PaymentCompleted")
+ .isCompleted();
+ }
+
+ @Test
+ @DisplayName("Use credit card")
+ public void payWithCreditCard() throws Exception {
+ var variables = Map.of(
+ "orderAmount", 60.0,
+ "customerCredit", 50.0,
+ "orderReference", "Order-1",
+ "cardExpiry", "01/2026",
+ "cardNumber", "1234567812345678",
+ "cardCVC", "111"
+ );
+
+ ProcessInstanceEvent processInstance = startProcess(client, "PaymentProcess", variables);
+ completeServiceTask(client, "credit-deduction", new CreditDeductionHandler());
+ completeUserTask(client, Map.of());
+ completeServiceTask(client, "credit-card-charging", new CreditCardChargingHandler());
+
+ // Wait for the engine to progress through the flow
+ engine.waitForIdleState(Duration.ofSeconds(1));
+
+ assertThat(processInstance)
+ .hasPassedElement("Task_DeductCredit")
+ .hasPassedElement("Task_ChargeCreditCard")
+ .hasPassedElement("EndEvent_PaymentCompleted")
+ .isCompleted();
+ }
+
+ @Test
+ @DisplayName("Use credit card assuming the credit is insufficient")
+ public void payWithCreditCardWithoutDeductingCredit() throws Exception {
+ var variables = Map.of(
+ "orderAmount", 60.0,
+ "openAmount", 60.0, // the output from the Deduct Credit task
+ "orderReference", "Order-1",
+ "cardExpiry", "01/2026",
+ "cardNumber", "1234567812345678",
+ "cardCVC", "111"
+ );
+
+ // Start the process after the Deduct Credit task
+ ProcessInstanceEvent processInstance = startProcessBefore(client, "PaymentProcess", "Gateway_CreditSufficient", variables);
+ // No need for the CreditDeductionHandler since the process starts after the Deduct Credit task
+ completeUserTask(client, Map.of());
+ completeServiceTask(client, "credit-card-charging", new CreditCardChargingHandler());
+
+ // Wait for the engine to progress through the flow
+ engine.waitForIdleState(Duration.ofSeconds(1));
+
+ assertThat(processInstance)
+ .hasPassedElement("Task_ChargeCreditCard")
+ .hasPassedElement("EndEvent_PaymentCompleted")
+ .isCompleted();
+ }
+
+}
diff --git a/java/testing/src/test/java/com/micasa/tutorial/Utils.java b/java/testing/src/test/java/com/micasa/tutorial/Utils.java
new file mode 100644
index 0000000..c5c5146
--- /dev/null
+++ b/java/testing/src/test/java/com/micasa/tutorial/Utils.java
@@ -0,0 +1,68 @@
+package com.micasa.tutorial;
+
+import io.camunda.zeebe.client.ZeebeClient;
+import io.camunda.zeebe.client.api.response.ActivateJobsResponse;
+import io.camunda.zeebe.client.api.response.ActivatedJob;
+import io.camunda.zeebe.client.api.response.ProcessInstanceEvent;
+import io.camunda.zeebe.client.api.worker.JobHandler;
+import io.camunda.zeebe.process.test.assertions.BpmnAssert;
+
+import java.util.Map;
+
+public class Utils {
+
+ private static final String USER_TASK = "io.camunda.zeebe:userTask";
+
+ public static ProcessInstanceEvent startProcess(ZeebeClient client, String processId, Map variables) {
+ ProcessInstanceEvent processInstance = client.newCreateInstanceCommand()
+ .bpmnProcessId(processId)
+ .latestVersion()
+ .variables(variables)
+ .send()
+ .join();
+
+ BpmnAssert.assertThat(processInstance).isStarted();
+
+ return processInstance;
+ }
+
+ public static ProcessInstanceEvent startProcessBefore(ZeebeClient client, String processId, String startingPointId, Map variables) {
+ ProcessInstanceEvent processInstance = client.newCreateInstanceCommand()
+ .bpmnProcessId(processId)
+ .latestVersion()
+ .variables(variables)
+ .startBeforeElement(startingPointId)
+ .send()
+ .join();
+
+ BpmnAssert.assertThat(processInstance).isStarted();
+
+ return processInstance;
+ }
+
+ public static void completeServiceTask(ZeebeClient client, String jobType, JobHandler handler) throws Exception {
+ ActivateJobsResponse activateJobsResponse = client.newActivateJobsCommand()
+ .jobType(jobType)
+ .maxJobsToActivate(1)
+ .send()
+ .join();
+
+ ActivatedJob firstJob = activateJobsResponse.getJobs().get(0);
+ handler.handle(client, firstJob);
+ }
+
+ public static void completeUserTask(ZeebeClient client, Map variables) throws Exception {
+ ActivateJobsResponse activateJobsResponse = client.newActivateJobsCommand()
+ .jobType(USER_TASK)
+ .maxJobsToActivate(1)
+ .send()
+ .join();
+
+ ActivatedJob firstJob = activateJobsResponse.getJobs().get(0);
+ client.newCompleteCommand(firstJob)
+ .variables(variables)
+ .send()
+ .join();
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index 2bc9d1b..d6323b0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -12,22 +12,29 @@
Camunda 8 Samples
- service-orchestration
+ java
UTF-8
+ 21
+ 3.11.0
+ 3.2.2
+
8.3.1
+ 2.0.9
5.10.1
2.2
+ 5.7.0
-
-
-
-
-
+
+ org.slf4j
+ slf4j-simple
+ ${sl4j.verion}
+ test
+
org.junit.jupiter
junit-jupiter
@@ -40,7 +47,12 @@
${hamcrest.version}
test
-
+
+ org.mockito
+ mockito-core
+ ${mockito.version}
+ test
+
@@ -48,12 +60,37 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.11.0
+ ${maven-compiler.version}
- 21
+ ${java.version}
--enable-preview
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ ${maven-surefire.version}
+
+ --enable-preview
+
+
+
diff --git a/service-orchestration/src/main/java/com/micasa/tutorial/handler/CreditCardServiceHandler.java b/service-orchestration/src/main/java/com/micasa/tutorial/handler/CreditCardServiceHandler.java
deleted file mode 100644
index ee4351a..0000000
--- a/service-orchestration/src/main/java/com/micasa/tutorial/handler/CreditCardServiceHandler.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package com.micasa.tutorial.handler;
-
-import com.micasa.tutorial.service.CreditCard;
-import com.micasa.tutorial.service.CreditCardServiceMock;
-import io.camunda.zeebe.client.api.response.ActivatedJob;
-import io.camunda.zeebe.client.api.worker.JobClient;
-import io.camunda.zeebe.client.api.worker.JobHandler;
-
-import java.time.YearMonth;
-import java.time.format.DateTimeFormatter;
-import java.util.Map;
-
-public class CreditCardServiceHandler implements JobHandler {
-
- CreditCardServiceMock service = new CreditCardServiceMock();
-
- @Override
- public void handle(JobClient client, ActivatedJob job) throws Exception {
- var confirmationNumber = service.chargeCreditCard(
- (String) job.getVariable("reference"),
- (Double) job.getVariable("amount"),
- new CreditCard(
- (String) job.getVariable("cardNumber"),
- YearMonth.parse((String) job.getVariable("cardExpiry"), DateTimeFormatter.ofPattern("MM/yyyy")),
- (String) job.getVariable("cardCVC")
- )
- );
-
- // Inform Zeebe that the job was completed successfully and pass along the confirmation number
- var outputVariables = Map.of("confirmation", confirmationNumber);
- client.newCompleteCommand(job.getKey())
- .variables(outputVariables)
- .send()
- .join();
- }
-}
diff --git a/service-orchestration/src/main/resources/application.yaml b/service-orchestration/src/main/resources/application.yaml
deleted file mode 100644
index b54bf72..0000000
--- a/service-orchestration/src/main/resources/application.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-# https://github.com/camunda-community-hub/spring-zeebe#configuring-camunda-platform-8-saas-connection
-# https://github.com/camunda-community-hub/spring-zeebe#additional-configuration-options
-
-zeebe.client:
- cloud:
- region: ont-1
- clusterId: 4784612d-1495-4f2a-951d-049c8b985f06
- clientId: jMNcIFp.GJ1-ZYK~Av-8zLHM7xmYCTLs
- clientSecret: dbz--V.YX9JzRzF_44iL9DNz.h0_1S-qpPbl~2xYt6f0ua5h3CWQ2wVulQ68b-Rm
diff --git a/spring-boot/pom.xml b/spring-boot/pom.xml
new file mode 100644
index 0000000..c6ac551
--- /dev/null
+++ b/spring-boot/pom.xml
@@ -0,0 +1,24 @@
+
+
+ 4.0.0
+
+
+ com.micasa.tutorial
+ camunda
+ 0.0.1-SNAPSHOT
+
+
+ camunda-spring-boot
+ 0.0.1-SNAPSHOT
+ spring-boot
+ pom
+
+
+
+ io.camunda.spring
+ spring-boot-starter-camunda
+ ${camunda.version}
+
+
+