diff --git a/docs/Camunda.adoc b/docs/Camunda.adoc
index c25eece..a47eec3 100644
--- a/docs/Camunda.adoc
+++ b/docs/Camunda.adoc
@@ -1,4 +1,6 @@
:figure-caption!:
+:source-highlighter: highlight.js
+:source-language: java
:imagesdir: res
:toc2:
@@ -122,9 +124,18 @@ If you don't do that and use assertions about BPMN elements that are after the s
=== Testing
+* How can we test that a task exited through the boundary event ?
+
+.Not working
+----
+assertThat(processInstance)
+ .hasPassedElement("BoundaryEvent_InvalidCardExpiryDate")
+----
+
+==== Java
+
* How can we avoid redeploying the BPMN diagram before each test ?
-[source, java]
----
@ZeebeProcessTest
public class ProcessTest {
diff --git a/pom.xml b/pom.xml
index 0c57812..0439fbc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,6 +13,7 @@
java
+ spring-boot
diff --git a/spring-boot/error-handling/README.md b/spring-boot/error-handling/README.md
new file mode 100644
index 0000000..b27611c
--- /dev/null
+++ b/spring-boot/error-handling/README.md
@@ -0,0 +1,3 @@
+# Error Handling
+
+This is the implementation for [Camunda 8 - Error Handling](https://academy.camunda.com/c8-error-handling).
\ No newline at end of file
diff --git a/spring-boot/error-handling/docs/Details.adoc b/spring-boot/error-handling/docs/Details.adoc
new file mode 100644
index 0000000..693bd23
--- /dev/null
+++ b/spring-boot/error-handling/docs/Details.adoc
@@ -0,0 +1,107 @@
+:figure-caption!:
+:source-highlighter: highlight.js
+:source-language: java
+:imagesdir: res
+:toc2:
+
+= Error Handling
+
+.Process
+image::paymentProcess.png[Process, role="thumb"]
+
+This example shows:
+
+* how to configure the client
+
+[cols="2a,2a"]
+|===
+|Java |Spring Boot
+|
+.`Main.java`
+----
+String ZEEBE_ADDRESS = "...";
+String ZEEBE_CLIENT_ID = "...";
+String ZEEBE_CLIENT_SECRET = "...";
+String ZEEBE_AUTHORIZATION_SERVER_URL = "...";
+String ZEEBE_TOKEN_AUDIENCE = "...";
+
+var credentialsProvider = new OAuthCredentialsProviderBuilder()
+ .authorizationServerUrl(ZEEBE_AUTHORIZATION_SERVER_URL)
+ .audience(ZEEBE_TOKEN_AUDIENCE)
+ .clientId(ZEEBE_CLIENT_ID)
+ .clientSecret(ZEEBE_CLIENT_SECRET)
+ .build();
+
+ZeebeClient client = ZeebeClient.newClientBuilder()
+ .gatewayAddress(ZEEBE_ADDRESS)
+ .credentialsProvider(credentialsProvider)
+ .build())
+----
+|
+[source, yaml]
+.`application.yaml`
+----
+zeebe.client:
+ cloud:
+ region: ...
+ clusterId: ...
+ clientId: ...
+ clientSecret: ...
+----
+|===
+
+* how to deploy process
+
+@SpringBootApplication
+@EnableZeebeClient
+@Deployment(resources = "classpath*:*.bpmn")
+
+
+* how to _wire_ a service task:
+
+[cols="1, 2a,2a"]
+|===
+||Java |Spring Boot
+
+|
+.`Main.java`
+----
+final var credentialsProvider = new OAuthCredentialsProviderBuilder()
+ .authorizationServerUrl(ZEEBE_AUTHORIZATION_SERVER_URL)
+ .audience(ZEEBE_TOKEN_AUDIENCE)
+ .clientId(ZEEBE_CLIENT_ID)
+ .clientSecret(ZEEBE_CLIENT_SECRET)
+ .build();
+
+final ZeebeClient client = ZeebeClient.newClientBuilder()
+ .gatewayAddress(ZEEBE_ADDRESS)
+ .credentialsProvider(credentialsProvider)
+ .build())
+----
+|===
+
+* how to handle a task
+
+.`CreditCardChargingHandler.java`
+----
+@Override
+public void handle(JobClient client, ActivatedJob job) throws Exception { <1>
+ var reference = (String) job.getVariable("orderReference"); <2>
+
+ var confirmationNumber = creditCardService.chargeCreditCard(reference); <3>
+
+ var outputVariables = Map.of("confirmation", confirmationNumber); <4>
+
+ client.newCompleteCommand(job.getKey()) <5>
+ .variables(outputVariables) <4>
+ .send()
+ .join();
+}
+----
+<1> Implement the ``JobHandler``'s `handle` method
+<2> Get variables from the `ActivatedJob`
+<3> Call the service
+<4> Add job's output to the ``ActivatedJob``'s variables
+<5> Notify the engine that the job completed successfully
+
+@JobWorker(type = "send-rejection")
\ No newline at end of file
diff --git a/spring-boot/error-handling/docs/res/ServiceTask-Definition.png b/spring-boot/error-handling/docs/res/ServiceTask-Definition.png
new file mode 100644
index 0000000..a279d3e
Binary files /dev/null and b/spring-boot/error-handling/docs/res/ServiceTask-Definition.png differ
diff --git a/spring-boot/error-handling/docs/res/paymentProcess.png b/spring-boot/error-handling/docs/res/paymentProcess.png
new file mode 100644
index 0000000..0fcc2ac
Binary files /dev/null and b/spring-boot/error-handling/docs/res/paymentProcess.png differ
diff --git a/spring-boot/error-handling/pom.xml b/spring-boot/error-handling/pom.xml
new file mode 100644
index 0000000..3100237
--- /dev/null
+++ b/spring-boot/error-handling/pom.xml
@@ -0,0 +1,26 @@
+
+
+ 4.0.0
+
+
+ com.micasa.tutorial
+ camunda-spring-boot
+ 0.0.1-SNAPSHOT
+
+
+ camunda-spring-boot-error-handling
+ 0.0.1-SNAPSHOT
+ error-handling
+ jar
+
+
+
+ io.camunda.spring
+ spring-boot-starter-camunda-test
+ ${camunda.version}
+ test
+
+
+
+
diff --git a/spring-boot/error-handling/src/main/java/com/micasa/tutorial/PaymentApplication.java b/spring-boot/error-handling/src/main/java/com/micasa/tutorial/PaymentApplication.java
new file mode 100644
index 0000000..1f7236f
--- /dev/null
+++ b/spring-boot/error-handling/src/main/java/com/micasa/tutorial/PaymentApplication.java
@@ -0,0 +1,15 @@
+package com.micasa.tutorial;
+
+import io.camunda.zeebe.spring.client.EnableZeebeClient;
+import io.camunda.zeebe.spring.client.annotation.Deployment;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+@EnableZeebeClient
+@Deployment(resources = "classpath*:*.bpmn")
+public class PaymentApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(PaymentApplication.class, args);
+ }
+}
\ No newline at end of file
diff --git a/spring-boot/error-handling/src/main/java/com/micasa/tutorial/exceptions/CreditCardServiceException.java b/spring-boot/error-handling/src/main/java/com/micasa/tutorial/exceptions/CreditCardServiceException.java
new file mode 100644
index 0000000..f436a4c
--- /dev/null
+++ b/spring-boot/error-handling/src/main/java/com/micasa/tutorial/exceptions/CreditCardServiceException.java
@@ -0,0 +1,9 @@
+package com.micasa.tutorial.exceptions;
+
+public class CreditCardServiceException extends RuntimeException {
+
+ public CreditCardServiceException(String message) {
+ super(message);
+ }
+
+}
diff --git a/spring-boot/error-handling/src/main/java/com/micasa/tutorial/exceptions/InvalidCreditCardException.java b/spring-boot/error-handling/src/main/java/com/micasa/tutorial/exceptions/InvalidCreditCardException.java
new file mode 100644
index 0000000..f197ea2
--- /dev/null
+++ b/spring-boot/error-handling/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/spring-boot/error-handling/src/main/java/com/micasa/tutorial/handlers/CreditCardChargingHandler.java b/spring-boot/error-handling/src/main/java/com/micasa/tutorial/handlers/CreditCardChargingHandler.java
new file mode 100644
index 0000000..92a5325
--- /dev/null
+++ b/spring-boot/error-handling/src/main/java/com/micasa/tutorial/handlers/CreditCardChargingHandler.java
@@ -0,0 +1,41 @@
+package com.micasa.tutorial.handlers;
+
+import com.micasa.tutorial.exceptions.CreditCardServiceException;
+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.spring.client.annotation.JobWorker;
+import io.camunda.zeebe.spring.client.exception.ZeebeBpmnError;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.YearMonth;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+
+@Component
+public class CreditCardChargingHandler {
+
+ @Autowired
+ private CreditCardService creditCardService;
+
+ @JobWorker(type = "chargeCreditCard", autoComplete = true)
+ public Map handle(ActivatedJob job) throws CreditCardServiceException {
+ System.out.println("charge credit card [ retries = " + job.getRetries() + " ]");
+ 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);
+ return Map.of("confirmation", confirmationNumber);
+ } catch (InvalidCreditCardException icce) {
+ throw new ZeebeBpmnError("invalidCreditCardException", icce.getMessage());
+ }
+ }
+}
diff --git a/spring-boot/error-handling/src/main/java/com/micasa/tutorial/services/CreditCard.java b/spring-boot/error-handling/src/main/java/com/micasa/tutorial/services/CreditCard.java
new file mode 100644
index 0000000..7b22cc3
--- /dev/null
+++ b/spring-boot/error-handling/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/spring-boot/error-handling/src/main/java/com/micasa/tutorial/services/CreditCardService.java b/spring-boot/error-handling/src/main/java/com/micasa/tutorial/services/CreditCardService.java
new file mode 100644
index 0000000..5ab5fc7
--- /dev/null
+++ b/spring-boot/error-handling/src/main/java/com/micasa/tutorial/services/CreditCardService.java
@@ -0,0 +1,27 @@
+package com.micasa.tutorial.services;
+
+import com.micasa.tutorial.exceptions.CreditCardServiceException;
+import com.micasa.tutorial.exceptions.InvalidCreditCardException;
+import org.springframework.stereotype.Component;
+
+import java.time.YearMonth;
+import java.util.UUID;
+
+@Component
+public class CreditCardService {
+
+ public String chargeCreditCard(String transactionNumber, double amount, CreditCard creditCard) throws InvalidCreditCardException, CreditCardServiceException {
+ 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();
+ }
+ if (transactionNumber.equalsIgnoreCase("invalid")) {
+ var message = "The transaction number is invalid: " + transactionNumber;
+ System.out.println(message);
+ throw new CreditCardServiceException(message);
+ }
+ return UUID.randomUUID().toString();
+ }
+
+}
\ No newline at end of file
diff --git a/spring-boot/error-handling/src/main/resources/application.yaml b/spring-boot/error-handling/src/main/resources/application.yaml
new file mode 100644
index 0000000..045d638
--- /dev/null
+++ b/spring-boot/error-handling/src/main/resources/application.yaml
@@ -0,0 +1,9 @@
+# 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
\ No newline at end of file
diff --git a/spring-boot/error-handling/src/main/resources/checkError.form b/spring-boot/error-handling/src/main/resources/checkError.form
new file mode 100644
index 0000000..253f6b3
--- /dev/null
+++ b/spring-boot/error-handling/src/main/resources/checkError.form
@@ -0,0 +1,76 @@
+{
+ "executionPlatform": "Camunda Cloud",
+ "executionPlatformVersion": "8.2.0",
+ "exporter": {
+ "name": "Camunda Web Modeler",
+ "version": "8516401"
+ },
+ "schemaVersion": 10,
+ "components": [
+ {
+ "text": "## Check Credit Card Details",
+ "type": "text",
+ "id": "Field_1y7r1ul",
+ "layout": {
+ "row": "Row_1yzjcyo"
+ }
+ },
+ {
+ "label": "Reference",
+ "type": "textfield",
+ "layout": {
+ "row": "Row_1ammndy",
+ "columns": null
+ },
+ "id": "Field_035kqla",
+ "key": "reference"
+ },
+ {
+ "label": "Amount",
+ "type": "textfield",
+ "id": "Field_0p8a9xa",
+ "key": "amount",
+ "layout": {
+ "row": "Row_0h6eq07"
+ }
+ },
+ {
+ "label": "Card Number",
+ "type": "textfield",
+ "id": "Field_1j2py1a",
+ "key": "cardNumber",
+ "layout": {
+ "row": "Row_15opzdy"
+ }
+ },
+ {
+ "label": "Card Expiry",
+ "type": "textfield",
+ "id": "Field_1l2tmgg",
+ "key": "cardExpiry",
+ "layout": {
+ "row": "Row_0a2rkgg"
+ }
+ },
+ {
+ "label": "Card CVC",
+ "type": "textfield",
+ "id": "Field_0pydzhj",
+ "key": "cardCVC",
+ "layout": {
+ "row": "Row_1nh9icr"
+ }
+ },
+ {
+ "label": "Valid Credit Card?",
+ "type": "checkbox",
+ "id": "Field_08wi408",
+ "key": "isValidCreditCard",
+ "layout": {
+ "row": "Row_0fw9363"
+ }
+ }
+ ],
+ "type": "default",
+ "id": "checkError"
+}
\ No newline at end of file
diff --git a/spring-boot/error-handling/src/main/resources/paymentProcess.bpmn b/spring-boot/error-handling/src/main/resources/paymentProcess.bpmn
new file mode 100644
index 0000000..22b4b81
--- /dev/null
+++ b/spring-boot/error-handling/src/main/resources/paymentProcess.bpmn
@@ -0,0 +1,200 @@
+
+
+
+
+ {
+ "executionPlatform": "Camunda Cloud",
+ "executionPlatformVersion": "8.2.0",
+ "exporter": {
+ "name": "Camunda Web Modeler",
+ "version": "8516401"
+ },
+ "schemaVersion": 10,
+ "components": [
+ {
+ "text": "## Check Credit Card Details",
+ "type": "text",
+ "id": "Field_1y7r1ul",
+ "layout": {
+ "row": "Row_1yzjcyo"
+ }
+ },
+ {
+ "label": "Reference",
+ "type": "textfield",
+ "layout": {
+ "row": "Row_1c38750",
+ "columns": null
+ },
+ "id": "Field_18do177",
+ "key": "reference",
+ "readonly": false
+ },
+ {
+ "label": "Amount",
+ "type": "textfield",
+ "id": "Field_0p8a9xa",
+ "key": "amount",
+ "layout": {
+ "row": "Row_0h6eq07"
+ }
+ },
+ {
+ "label": "Card Number",
+ "type": "textfield",
+ "id": "Field_1j2py1a",
+ "key": "cardNumber",
+ "layout": {
+ "row": "Row_15opzdy"
+ }
+ },
+ {
+ "label": "Card Expiry",
+ "type": "textfield",
+ "id": "Field_1l2tmgg",
+ "key": "cardExpiry",
+ "layout": {
+ "row": "Row_0a2rkgg"
+ }
+ },
+ {
+ "label": "Card CVC",
+ "type": "textfield",
+ "id": "Field_0pydzhj",
+ "key": "cardCVC",
+ "layout": {
+ "row": "Row_1nh9icr"
+ }
+ },
+ {
+ "label": "Valid Credit Card?",
+ "type": "checkbox",
+ "id": "Field_08wi408",
+ "key": "isValidCreditCard",
+ "layout": {
+ "row": "Row_0fw9363"
+ }
+ }
+ ],
+ "type": "default",
+ "id": "checkError"
+}
+
+
+ Flow_paymentRequired-chargeCreditCard
+
+
+
+
+
+
+ Flow_paymentRequired-chargeCreditCard
+ Flow_Gateway_Resolved-Task_ChargeCreditCard
+ Flow_chargeCreditCard-paymentSuccessful
+
+
+ Flow_chargeCreditCard-paymentSuccessful
+
+
+
+ Flow_00zdqb3
+
+
+
+
+
+
+ Flow_00zdqb3
+ Flow_09afd84
+
+
+ Flow_09afd84
+ Flow_GatewayResolved-EndEvent_PaymentCancelled
+ Flow_Gateway_Resolved-Task_ChargeCreditCard
+
+
+ Flow_GatewayResolved-EndEvent_PaymentCancelled
+
+
+
+
+ =isValidCreditCard = false
+
+
+ =isValidCreditCard = true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-boot/error-handling/src/test/java/com/micasa/tutorial/PaymentProcessTest.java b/spring-boot/error-handling/src/test/java/com/micasa/tutorial/PaymentProcessTest.java
new file mode 100644
index 0000000..5c11f88
--- /dev/null
+++ b/spring-boot/error-handling/src/test/java/com/micasa/tutorial/PaymentProcessTest.java
@@ -0,0 +1,176 @@
+package com.micasa.tutorial;
+
+import io.camunda.zeebe.client.ZeebeClient;
+import io.camunda.zeebe.client.api.response.ProcessInstanceEvent;
+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.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.micasa.tutorial.Utils.*;
+import static io.camunda.zeebe.process.test.assertions.BpmnAssert.assertThat;
+import static io.camunda.zeebe.spring.test.ZeebeTestThreadSupport.waitForProcessInstanceCompleted;
+
+@SpringBootTest
+@ZeebeSpringTest
+@DisplayName("Payment Process Test")
+public class PaymentProcessTest {
+
+ @Autowired
+ private ZeebeTestEngine engine;
+
+ @Autowired
+ private ZeebeClient client;
+
+ @Test
+ @DisplayName("Successful payment")
+ public void success() throws Exception {
+ var variables = Map.of(
+ "orderAmount", 60.0,
+ "orderReference", "Order-1",
+ "cardExpiry", "01/2026",
+ "cardNumber", "1234567812345678",
+ "cardCVC", "111"
+ );
+
+ ProcessInstanceEvent processInstance = startProcess(client, "PaymentProcess", variables);
+
+ // Wait for the engine to progress through the flow
+// engine.waitForIdleState(Duration.ofSeconds(1));
+ waitForProcessInstanceCompleted(processInstance);
+
+ assertThat(processInstance)
+ .hasPassedElement("Task_ChargeCreditCard")
+ .hasPassedElement("EndEvent_PaymentCompleted")
+ .isCompleted();
+ }
+
+ @Test
+ @DisplayName("Unexpected handler failure -> incident")
+ public void failure() throws Exception {
+ var variables = Map.of(
+ "orderAmount", 60.0,
+ "cardExpiry", "01/2026",
+ "cardNumber", "1234567812345678",
+ "cardCVC", "111"
+ );
+
+ // Start the process after the Deduct Credit task
+ ProcessInstanceEvent processInstance = startProcess(client, "PaymentProcess", variables);
+
+ // Wait for the engine to progress through the flow
+ engine.waitForIdleState(Duration.ofSeconds(10));
+
+ assertThat(processInstance)
+ .hasNotPassedElement("Task_ChargeCreditCard")
+ .hasNotPassedElement("EndEvent_PaymentCompleted")
+ .hasAnyIncidents();
+ }
+
+ @Test
+ @DisplayName("Service failure -> incident")
+ public void incident() throws Exception {
+ var variables = Map.of(
+ "orderAmount", 60.0,
+ "orderReference", "invalid",
+ "cardExpiry", "01/2026",
+ "cardNumber", "1234567812345678",
+ "cardCVC", "111"
+ );
+
+ // Start the process after the Deduct Credit task
+ ProcessInstanceEvent processInstance = startProcess(client, "PaymentProcess", variables);
+
+ // Wait for the engine to progress through the flow
+ engine.waitForIdleState(Duration.ofSeconds(10));
+
+ assertThat(processInstance)
+ .hasNotPassedElement("Task_ChargeCreditCard")
+ .hasNotPassedElement("EndEvent_PaymentCompleted")
+ .hasAnyIncidents();
+ }
+
+ @Test
+ @DisplayName("Invalid expiry date -> BPMN Error with fix")
+ public void errorWithFix() throws Exception {
+ Map variables = Map.of(
+ "orderAmount", 60.0,
+ "orderReference", "Order-1",
+ "cardExpiry", "01/2023",
+ "cardNumber", "1234567812345678",
+ "cardCVC", "111"
+ );
+
+ // Start the process after the Deduct Credit task
+ ProcessInstanceEvent processInstance = startProcess(client, "PaymentProcess", variables);
+
+ // Wait for the engine to progress through the flow
+ engine.waitForIdleState(Duration.ofSeconds(1));
+
+ assertThat(processInstance)
+ .hasNotPassedElement("Task_ChargeCreditCard")
+ .hasNoIncidents()
+ .isActive();
+
+ var newVariables = new HashMap<>(variables);
+ newVariables.replace("cardExpiry", "01/2033");
+ newVariables.put("isValidCreditCard", true);
+
+ completeUserTask(client, newVariables);
+
+ waitForProcessInstanceCompleted(processInstance);
+
+ assertThat(processInstance)
+ .hasPassedElement("Task_ChargeCreditCard")
+ .hasPassedElement("EndEvent_PaymentCompleted")
+ .hasNoIncidents()
+ .isCompleted();
+
+ }
+
+ @Test
+ @DisplayName("Invalid expiry date -> BPMN Error without fix")
+ public void errorWithoutFix() throws Exception {
+ Map variables = Map.of(
+ "orderAmount", 60.0,
+ "orderReference", "Order-1",
+ "cardExpiry", "01/2023",
+ "cardNumber", "1234567812345678",
+ "cardCVC", "111"
+ );
+
+ // Start the process after the Deduct Credit task
+ ProcessInstanceEvent processInstance = startProcess(client, "PaymentProcess", variables);
+
+ // Wait for the engine to progress through the flow
+ engine.waitForIdleState(Duration.ofSeconds(1));
+
+ assertThat(processInstance)
+ .hasNotPassedElement("Task_ChargeCreditCard")
+ .hasNoIncidents()
+ .isActive();
+
+ var newVariables = new HashMap<>(variables);
+ newVariables.put("isValidCreditCard", false);
+
+ completeUserTask(client, newVariables);
+
+ waitForProcessInstanceCompleted(processInstance);
+
+ assertThat(processInstance)
+ .hasNotPassedElement("Task_ChargeCreditCard")
+ .hasNotPassedElement("EndEvent_PaymentCompleted")
+ .hasPassedElement("EndEvent_PaymentCancelled")
+ .hasNoIncidents()
+ .isCompleted();
+
+ }
+
+}
diff --git a/spring-boot/error-handling/src/test/java/com/micasa/tutorial/Utils.java b/spring-boot/error-handling/src/test/java/com/micasa/tutorial/Utils.java
new file mode 100644
index 0000000..c5c5146
--- /dev/null
+++ b/spring-boot/error-handling/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/spring-boot/pom.xml b/spring-boot/pom.xml
index c6ac551..5a52e6e 100644
--- a/spring-boot/pom.xml
+++ b/spring-boot/pom.xml
@@ -14,6 +14,10 @@
spring-boot
pom
+
+ error-handling
+
+
io.camunda.spring