initial commit

This commit is contained in:
Luc Weinbrecht
2022-05-02 07:25:27 +02:00
commit a8d2aa9342
59 changed files with 2320 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
name: Build & Push (Docker)
on:
push:
branches:
- 'main'
jobs:
build-and-push:
name: Build JAR and push Docker image
runs-on: ubuntu-18.04
env:
repo: lwluc/camunda-ddd-and-clean-architecture
steps:
- uses: actions/checkout@v1
- name: Set up JDK 17
uses: actions/setup-java@v1
with:
java-version: 17
- name: Maven Package
run: mvn clean package
- name: Login to Docker Hub
run: docker login -u ${{ secrets.DOCKER_USER }} -p '${{ secrets.DOCKER_TOKEN }}'
- name: Build Docker image
run: docker build -t ${{env.repo}} .
- name: Publish Docker image
run: docker push ${{env.repo}}

48
.gitignore vendored Normal file
View File

@@ -0,0 +1,48 @@
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# Maven
log/
target/
.classpath
.settings
.project
**.db
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
# OS generated files #
######################
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Intellij
.idea/
*.iml
*.iws

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM maven:3.8.5-openjdk-17 as build
COPY pom.xml .
RUN mvn -B dependency:go-offline
COPY src src
RUN mvn -B package
FROM openjdk:11-jre-slim-buster
COPY --from=build target/camunda-ddd-and-clean-architecture.jar .
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "camunda-ddd-and-clean-architecture.jar"]

54
README.md Normal file
View File

@@ -0,0 +1,54 @@
# Camunda DDD and Clean Architecture
An example to show how you could use clean architecture and DDD elements with Camunda.
## 🚀Features
The [BPMN process](./assets/loan_agreement.png) is a tiny process just to demonstrate the architecture.
With the following POST request you could start the process:
```curl
curl --request POST \
--url http://localhost:8080/loan/agreement/1 \
--header 'Content-Type: application/json' \
--cookie JSESSIONID=9E203C2691A2F320151C467311C720D1 \
--data '
"customerNumber": "A-11",
"name": "Tester",
"mailAddress": "tester@web.io",
"amount": 1100
}'
```
Using the admin user (`username: admin` and `password: pw`) you could log in to the Camunda Cockpit.
## 🏗Architecture
In the following sections contains some small aspects explaining the advantages of Domain Driven Design (DDD) and clean architecture.
![DDD-Clean-Architecture](./assets/camunda-ddd-and-clean-architecture.png)
### DDD
Using Domain Drive Design or to be more precise tactical DDD you could be way more expressive and closer to your business domain. Beside that the focus in *immutability* and building object that know all about their *invariants* helps you to structure your code. Such DDD Elements can be found in our [domain-primitives](https://github.com/domain-primitives/domain-primitives-java) library.
Structuring your code functional and brining more context to your object with, e.g. Value Object does not only help you to keep your code expressive, it also helps keeping it close to you business as your BPMN model.
### Clean
Using clean architecture as architecture style combines perfectly with Domain Driven Design, because we completely focus on our domain.
Flexibility around your domain is the main focus I want to show you in this little example.
The main advantage is the independence of any framework. Due to this fact the architecture allows, compared to the conventional layer-architecture, exchange for example you Camunda Framework. So you could migrate to Camunda 8 without even touching your business code.
[The origin of clean architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html).
## 🙏🏼Credits
Thanks to [Matthias Eschhold](https://github.com/MatthiasEschhold) for the passionate discussion around DDD and clean architecture.
## 📨Contact
If you have any questions or ideas feel free to contact me or create an [issue](https://github.com/lwluc/camunda-ddd-and-clean-architecture/issues).

View File

@@ -0,0 +1,536 @@
{
"type": "excalidraw",
"version": 2,
"source": "https://excalidraw.com",
"elements": [
{
"type": "ellipse",
"version": 419,
"versionNonce": 1353777024,
"isDeleted": false,
"id": "Euc_Y36uu1tj7wSxPMAKo",
"fillStyle": "hachure",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 188.9761692951783,
"y": 503.8009634020121,
"strokeColor": "#000",
"backgroundColor": "#f39000",
"width": 626.3757259147652,
"height": 626.3757259147652,
"seed": 2072553600,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1651467138757,
"link": null,
"locked": false
},
{
"id": "ufXZhHDFuihic0jU210X8",
"type": "ellipse",
"x": 257.6329680816313,
"y": 579.0412908392208,
"width": 481.53809559813783,
"height": 481.53809559813783,
"angle": 0,
"strokeColor": "#000",
"backgroundColor": "#1faf98",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"strokeSharpness": "sharp",
"seed": 744693632,
"version": 236,
"versionNonce": 1099552896,
"isDeleted": false,
"boundElements": null,
"updated": 1651467092353,
"link": null,
"locked": false
},
{
"type": "text",
"version": 533,
"versionNonce": 146319488,
"isDeleted": false,
"id": "hPRUDN6SwwxNALCFa6rYB",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 431.9020158807,
"y": 608.5664954874767,
"strokeColor": "#000",
"backgroundColor": "#ced4da",
"width": 133,
"height": 35,
"seed": 1164144512,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1651467106457,
"link": null,
"locked": false,
"fontSize": 28,
"fontFamily": 1,
"text": "Use Case",
"baseline": 25,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"originalText": "Use Case"
},
{
"type": "text",
"version": 606,
"versionNonce": 1956728704,
"isDeleted": false,
"id": "O6LsqUbz9_Git5cLAzGhJ",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 444.78302406663033,
"y": 525.8021353065467,
"strokeColor": "#000",
"backgroundColor": "#ced4da",
"width": 111,
"height": 35,
"seed": 1772998528,
"groupIds": [],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1651467142240,
"link": null,
"locked": false,
"fontSize": 28,
"fontFamily": 1,
"text": "Adapter",
"baseline": 25,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"originalText": "Adapter"
},
{
"type": "ellipse",
"version": 1001,
"versionNonce": 1254697088,
"isDeleted": false,
"id": "DUaacDwmSlxMWxQc9k7RX",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 339.3006834918491,
"y": 669.1363581442278,
"strokeColor": "#000000",
"backgroundColor": "#fff",
"width": 318.37543728901085,
"height": 318.3754372890108,
"seed": 984932224,
"groupIds": [
"B0twdI68Hz7VWMi4UlAOW"
],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1651467104973,
"link": null,
"locked": false
},
{
"type": "ellipse",
"version": 914,
"versionNonce": 1810304,
"isDeleted": false,
"id": "D4JhsJph47k2QoB9fNuJK",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 338.8304314453659,
"y": 669.2850979118147,
"strokeColor": "#000000",
"backgroundColor": "#36bcee",
"width": 318.37543728901085,
"height": 318.3754372890108,
"seed": 522901632,
"groupIds": [
"B0twdI68Hz7VWMi4UlAOW"
],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1651467104973,
"link": null,
"locked": false
},
{
"type": "text",
"version": 574,
"versionNonce": 1485838464,
"isDeleted": false,
"id": "o0Agc55hL44xHW-ZToF4P",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 445.9289062293193,
"y": 695.1180406103032,
"strokeColor": "#000",
"backgroundColor": "#ced4da",
"width": 107,
"height": 35,
"seed": 1609363328,
"groupIds": [
"B0twdI68Hz7VWMi4UlAOW"
],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1651467104973,
"link": null,
"locked": false,
"fontSize": 28,
"fontFamily": 1,
"text": "Entities",
"baseline": 25,
"textAlign": "center",
"verticalAlign": "top",
"containerId": null,
"originalText": "Entities"
},
{
"type": "rectangle",
"version": 920,
"versionNonce": 1249619840,
"isDeleted": false,
"id": "tYHC-hmS7pDvbvw9mqXFg",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 443.939164744378,
"y": 907.9454124118723,
"strokeColor": "#000",
"backgroundColor": "transparent",
"width": 108,
"height": 59,
"seed": 356239488,
"groupIds": [
"B0twdI68Hz7VWMi4UlAOW"
],
"strokeSharpness": "sharp",
"boundElements": [
{
"id": "ar58-5fnU7z_xVgO1qNsi",
"type": "text"
},
{
"type": "text",
"id": "ar58-5fnU7z_xVgO1qNsi"
}
],
"updated": 1651467104973,
"link": null,
"locked": false
},
{
"type": "text",
"version": 915,
"versionNonce": 1878591616,
"isDeleted": false,
"id": "ar58-5fnU7z_xVgO1qNsi",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 448.939164744378,
"y": 917.4454124118723,
"strokeColor": "#000",
"backgroundColor": "#ced4da",
"width": 98,
"height": 40,
"seed": 1134755712,
"groupIds": [
"B0twdI68Hz7VWMi4UlAOW"
],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1651467104973,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 1,
"text": "<Value \nObject>",
"baseline": 34,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "tYHC-hmS7pDvbvw9mqXFg",
"originalText": "<Value Object>"
},
{
"type": "rectangle",
"version": 936,
"versionNonce": 1851375488,
"isDeleted": false,
"id": "Un4YWq1C54B8PnUiK5agt",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 512.2046968298423,
"y": 838.9442405073372,
"strokeColor": "#000",
"backgroundColor": "transparent",
"width": 108,
"height": 59,
"seed": 2075048064,
"groupIds": [
"B0twdI68Hz7VWMi4UlAOW"
],
"strokeSharpness": "sharp",
"boundElements": [
{
"id": "tRDt4Vip1_Ois0oz3rbzx",
"type": "text"
},
{
"id": "tRDt4Vip1_Ois0oz3rbzx",
"type": "text"
},
{
"type": "text",
"id": "tRDt4Vip1_Ois0oz3rbzx"
}
],
"updated": 1651467104973,
"link": null,
"locked": false
},
{
"type": "text",
"version": 923,
"versionNonce": 214169728,
"isDeleted": false,
"id": "tRDt4Vip1_Ois0oz3rbzx",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 517.2046968298423,
"y": 858.4442405073372,
"strokeColor": "#000",
"backgroundColor": "#ced4da",
"width": 98,
"height": 20,
"seed": 1712787328,
"groupIds": [
"B0twdI68Hz7VWMi4UlAOW"
],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1651467104973,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 1,
"text": "<Entity>",
"baseline": 14,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "Un4YWq1C54B8PnUiK5agt",
"originalText": "<Entity>"
},
{
"type": "rectangle",
"version": 966,
"versionNonce": 200658816,
"isDeleted": false,
"id": "dfPlLg74polPs4cRMjLMH",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 387.1176524654826,
"y": 839.8847446003026,
"strokeColor": "#000",
"backgroundColor": "transparent",
"width": 108,
"height": 59,
"seed": 1984145536,
"groupIds": [
"B0twdI68Hz7VWMi4UlAOW"
],
"strokeSharpness": "sharp",
"boundElements": [
{
"id": "nF3MdAxVTmtMBuLT2YClm",
"type": "text"
},
{
"id": "nF3MdAxVTmtMBuLT2YClm",
"type": "text"
},
{
"id": "nF3MdAxVTmtMBuLT2YClm",
"type": "text"
},
{
"type": "text",
"id": "nF3MdAxVTmtMBuLT2YClm"
}
],
"updated": 1651467104973,
"link": null,
"locked": false
},
{
"type": "text",
"version": 961,
"versionNonce": 850454656,
"isDeleted": false,
"id": "nF3MdAxVTmtMBuLT2YClm",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 392.1176524654826,
"y": 859.3847446003026,
"strokeColor": "#000",
"backgroundColor": "#ced4da",
"width": 98,
"height": 20,
"seed": 1065951104,
"groupIds": [
"B0twdI68Hz7VWMi4UlAOW"
],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1651467104973,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 1,
"text": "<Aggregate>",
"baseline": 14,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "dfPlLg74polPs4cRMjLMH",
"originalText": "<Aggregate>"
},
{
"type": "rectangle",
"version": 985,
"versionNonce": 999284608,
"isDeleted": false,
"id": "IHNH37dUIbqk92l6XDzjC",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 444.488402136354,
"y": 749.5963516756522,
"strokeColor": "#000",
"backgroundColor": "transparent",
"width": 108,
"height": 59,
"seed": 1348943744,
"groupIds": [
"B0twdI68Hz7VWMi4UlAOW"
],
"strokeSharpness": "sharp",
"boundElements": [
{
"id": "w0udDoif6KaJf9if4gBJ-",
"type": "text"
},
{
"id": "w0udDoif6KaJf9if4gBJ-",
"type": "text"
},
{
"id": "w0udDoif6KaJf9if4gBJ-",
"type": "text"
},
{
"type": "text",
"id": "w0udDoif6KaJf9if4gBJ-"
}
],
"updated": 1651467104973,
"link": null,
"locked": false
},
{
"type": "text",
"version": 978,
"versionNonce": 1837095040,
"isDeleted": false,
"id": "w0udDoif6KaJf9if4gBJ-",
"fillStyle": "cross-hatch",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"angle": 0,
"x": 449.488402136354,
"y": 769.0963516756522,
"strokeColor": "#000",
"backgroundColor": "#ced4da",
"width": 98,
"height": 20,
"seed": 1011775616,
"groupIds": [
"B0twdI68Hz7VWMi4UlAOW"
],
"strokeSharpness": "sharp",
"boundElements": [],
"updated": 1651467104973,
"link": null,
"locked": false,
"fontSize": 16,
"fontFamily": 1,
"text": "<Service>",
"baseline": 14,
"textAlign": "center",
"verticalAlign": "middle",
"containerId": "IHNH37dUIbqk92l6XDzjC",
"originalText": "<Service>"
}
],
"appState": {
"gridSize": null,
"viewBackgroundColor": "#ffffff"
},
"files": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

BIN
assets/loan_agreement.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

138
pom.xml Normal file
View File

@@ -0,0 +1,138 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>de.weinbrecht.luc.bpm.architecture</groupId>
<artifactId>camunda-ddd-and-clean-architecture</artifactId>
<version>1.0.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<version.junit5>5.8.2</version.junit5>
<version.lombok>1.18.24</version.lombok>
<version.domainprimitives>0.1.0</version.domainprimitives>
<version.bpmAssert>1.1.0</version.bpmAssert>
<version.camundaMockito>6.17.0</version.camundaMockito>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.6.4</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.camunda.bpm</groupId>
<artifactId>camunda-bom</artifactId>
<version>7.17.0</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-rest</artifactId>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-webapp</artifactId>
</dependency>
<dependency>
<groupId>org.camunda.bpm.springboot</groupId>
<artifactId>camunda-bpm-spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>io.github.domain-primitives</groupId>
<artifactId>domainprimitives-java</artifactId>
<version>${version.domainprimitives}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${version.lombok}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${version.junit5}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${version.junit5}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${version.junit5}</version>
<scope>test</scope>
</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>
</dependencies>
<build>
<finalName>${artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.4</version>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,13 @@
package de.weinbrecht.luc.bpm.architecture;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String... args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -0,0 +1,10 @@
package de.weinbrecht.luc.bpm.architecture.common;
public class ProcessConstants {
public static final String PROCESS_DEFINITION = "Loan_Agreement";
public static final String START_EVENT_MESSAGE_REF = "loanAgreementReceivedMessage";
public static final String LOAN_AGREEMENT_NUMBER = "loanAgreementNumber";
}

View File

@@ -0,0 +1,25 @@
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.usecase.in.LoanAgreementStatusCommand;
import lombok.RequiredArgsConstructor;
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 static de.weinbrecht.luc.bpm.architecture.common.ProcessConstants.LOAN_AGREEMENT_NUMBER;
@Slf4j
@RequiredArgsConstructor
@Component
public class ApproveLoanAgreement implements JavaDelegate {
private final LoanAgreementStatusCommand loanAgreementStatusCommand;
@Override
public void execute(DelegateExecution delegateExecution) {
Long loanAgreementNumber = (Long) delegateExecution.getVariable(LOAN_AGREEMENT_NUMBER);
loanAgreementStatusCommand.accept(new LoanAgreementNumber(loanAgreementNumber));
}
}

View File

@@ -0,0 +1,25 @@
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.usecase.in.LoanAgreementStatusCommand;
import lombok.RequiredArgsConstructor;
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 static de.weinbrecht.luc.bpm.architecture.common.ProcessConstants.LOAN_AGREEMENT_NUMBER;
@Slf4j
@RequiredArgsConstructor
@Component
public class RejectionLoanAgreement implements JavaDelegate {
private final LoanAgreementStatusCommand loanAgreementStatusCommand;
@Override
public void execute(DelegateExecution delegateExecution) {
Long loanAgreementNumber = (Long) delegateExecution.getVariable(LOAN_AGREEMENT_NUMBER);
loanAgreementStatusCommand.reject(new LoanAgreementNumber(loanAgreementNumber));
}
}

View File

@@ -0,0 +1,21 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.web;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.CaseId;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in.LoanAgreementCreation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
@RequiredArgsConstructor
@RestController
@RequestMapping("loan/agreement/")
class LoanAgreementController {
private final LoanAgreementCreation loanAgreementCreation;
private final LoanAgreementMapper mapper;
@PostMapping("{caseId}")
public void create(@RequestBody LoanAgreementResource loanAgreementResource,
@PathVariable String caseId) {
loanAgreementCreation.create(mapper.mapToDomain(loanAgreementResource), new CaseId(caseId));
}
}

View File

@@ -0,0 +1,23 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.web;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.Amount;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.CustomerNumber;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.Recipient;
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 org.springframework.stereotype.Component;
@Component("LoanAgreementMapperWeb")
class LoanAgreementMapper {
public LoanAgreement mapToDomain(LoanAgreementResource loanAgreementResource) {
return new LoanAgreement(
new Recipient(
new CustomerNumber(loanAgreementResource.getCustomerNumber()),
new Name(loanAgreementResource.getName()),
new MailAddress(loanAgreementResource.getMailAddress())
),
new Amount(loanAgreementResource.getAmount())
);
}
}

View File

@@ -0,0 +1,11 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.web;
import lombok.Data;
@Data
class LoanAgreementResource {
private String customerNumber;
private String name;
private String mailAddress;
private Integer amount;
}

View File

@@ -0,0 +1,8 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.db;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface LoanAgreementCRUDRepository extends CrudRepository<LoanAgreementEntity, Long> {
}

View File

@@ -0,0 +1,22 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.db;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import static javax.persistence.GenerationType.AUTO;
@Data
@Entity
class LoanAgreementEntity {
@Id
@GeneratedValue(strategy= AUTO)
private Long id;
private Integer amount;
private String customerNumber;
private String name;
private String mailAddress;
}

View File

@@ -0,0 +1,31 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.db;
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 org.springframework.stereotype.Component;
@Component("LoanAgreementMapperDb")
class LoanAgreementMapper {
public LoanAgreementEntity mapToDb(LoanAgreement loanAgreement) {
LoanAgreementEntity loanAgreementEntity = new LoanAgreementEntity();
loanAgreementEntity.setAmount(loanAgreement.getAmount().getValue());
Recipient recipient = loanAgreement.getRecipient();
loanAgreementEntity.setCustomerNumber(recipient.getCustomerNumber().getValue());
loanAgreementEntity.setName(recipient.getName().getValue());
loanAgreementEntity.setMailAddress(recipient.getMailAddress().getValue());
return loanAgreementEntity;
}
public LoanAgreement mapToDomain(LoanAgreementEntity loanAgreementEntity) {
return new LoanAgreement(
new LoanAgreementNumber(loanAgreementEntity.getId()),
new Recipient(
new CustomerNumber(loanAgreementEntity.getCustomerNumber()),
new Name(loanAgreementEntity.getName()),
new MailAddress(loanAgreementEntity.getMailAddress())
),
new Amount(loanAgreementEntity.getAmount())
);
}
}

View File

@@ -0,0 +1,7 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.db;
class LoanAgreementNotFoundException extends RuntimeException {
public LoanAgreementNotFoundException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,32 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.db;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementCommand;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementQuery;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import static java.lang.String.format;
@RequiredArgsConstructor
@Component
class LoanAgreementRepository implements LoanAgreementCommand, LoanAgreementQuery {
private final LoanAgreementCRUDRepository crudRepository;
private final LoanAgreementMapper mapper;
@Override
public LoanAgreementNumber save(LoanAgreement loanAgreement) {
LoanAgreementEntity savedLoanAgreement = crudRepository.save(mapper.mapToDb(loanAgreement));
return new LoanAgreementNumber(savedLoanAgreement.getId());
}
@Override
public LoanAgreement loadByNumber(LoanAgreementNumber loanAgreementNumber) {
return crudRepository.findById(loanAgreementNumber.getValue())
.map(mapper::mapToDomain)
.orElseThrow(() -> new LoanAgreementNotFoundException(
format("Could not find loan agreement to number %s", loanAgreementNumber.getValue())));
}
}

View File

@@ -0,0 +1,16 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.legacy;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementDistributor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class LoanAgreementDistributorClient implements LoanAgreementDistributor {
@Override
public void sendLoanAgreement(LoanAgreement loanAgreement, boolean accepted) {
log.info("Sending loan agreement with number {} and status [{}] to legacy system",
loanAgreement.getLoanAgreementNumber().getValue(), accepted);
}
}

View File

@@ -0,0 +1,28 @@
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.LoanAgreementNumber;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.ProcessEngineCommand;
import lombok.RequiredArgsConstructor;
import org.camunda.bpm.engine.RuntimeService;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
import static de.weinbrecht.luc.bpm.architecture.common.ProcessConstants.LOAN_AGREEMENT_NUMBER;
import static de.weinbrecht.luc.bpm.architecture.common.ProcessConstants.START_EVENT_MESSAGE_REF;
@RequiredArgsConstructor
@Component
class ProcessEngineClient implements ProcessEngineCommand {
private final RuntimeService runtimeService;
@Override
public void startLoanAgreement(CaseId caseId, LoanAgreementNumber loanAgreementNumber) {
Map<String, Object> processVariables = new HashMap<>();
processVariables.put(LOAN_AGREEMENT_NUMBER, loanAgreementNumber.getValue());
runtimeService.startProcessInstanceByMessage(START_EVENT_MESSAGE_REF, caseId.getValue(), processVariables);
}
}

View File

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

View File

@@ -0,0 +1,22 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model;
import io.github.domainprimitives.type.ValueObject;
import static io.github.domainprimitives.validation.Constraints.isGreatThanOrEqual;
public class Amount extends ValueObject<Integer> {
public Amount(Integer value) {
super(value, isGreatThanOrEqual(100));
}
@Override
public boolean equals(Object o) {
if (o != null && this.getClass() == o.getClass()) {
ValueObject<Integer> valueObject = (ValueObject)o;
return (valueObject.getValue()).equals(this.getValue());
} else {
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model;
import io.github.domainprimitives.type.ValueObject;
import static io.github.domainprimitives.validation.Constraints.isNotBlank;
public class CaseId extends ValueObject<String> {
public CaseId(String value) {
super(value, isNotBlank());
}
}

View File

@@ -0,0 +1,11 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model;
import io.github.domainprimitives.type.ValueObject;
import static io.github.domainprimitives.validation.Constraints.isNotBlank;
public class CustomerNumber extends ValueObject<String> {
public CustomerNumber(String value) {
super(value, isNotBlank());
}
}

View File

@@ -0,0 +1,34 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model;
import io.github.domainprimitives.object.Aggregate;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode(callSuper = false)
public class LoanAgreement extends Aggregate {
private LoanAgreementNumber loanAgreementNumber;
private final Recipient recipient;
private final Amount amount;
public LoanAgreement(Recipient recipient, Amount amount) {
this.recipient = recipient;
this.amount = amount;
this.validate();
}
public LoanAgreement(LoanAgreementNumber loanAgreementNumber, Recipient recipient, Amount amount) {
this(recipient, amount);
this.loanAgreementNumber = loanAgreementNumber;
validateNotNull(loanAgreementNumber, "Loan Agreement Number");
evaluateValidations();
}
protected void validate() {
validateNotNull(recipient, "Recipient");
validateNotNull(amount, "Loan Amount");
evaluateValidations();
}
}

View File

@@ -0,0 +1,11 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model;
import io.github.domainprimitives.type.ValueObject;
import static io.github.domainprimitives.validation.Constraints.isNotNullLong;
public class LoanAgreementNumber extends ValueObject<Long> {
public LoanAgreementNumber(Long value) {
super(value, isNotNullLong());
}
}

View File

@@ -0,0 +1,31 @@
package 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 io.github.domainprimitives.object.ComposedValueObject;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode(callSuper = false)
public class Recipient extends ComposedValueObject {
private final CustomerNumber customerNumber;
private final Name name;
private final MailAddress mailAddress;
public Recipient(CustomerNumber customerNumber, Name name, MailAddress mailAddress) {
this.customerNumber = customerNumber;
this.name = name;
this.mailAddress = mailAddress;
this.validate();
}
@Override
protected void validate() {
validateNotNull(customerNumber, "Customer Number");
validateNotNull(name, "Name");
validateNotNull(mailAddress, "Mail Address");
evaluateValidations();
}
}

View File

@@ -0,0 +1,19 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.recipient;
import io.github.domainprimitives.type.ValueObject;
import java.util.regex.Pattern;
import static io.github.domainprimitives.validation.Constraints.isPattern;
public class MailAddress extends ValueObject<String> {
public static final String VALID_EMAIL_ADDRESS_REGEX =
Pattern.compile("^[a-zA-Z0-9_!#$%&*+=?`{|}~^.-]+@[a-zA-Z0-9.-]+$",
Pattern.CASE_INSENSITIVE).pattern();
public MailAddress(String value) {
super(value, isPattern(VALID_EMAIL_ADDRESS_REGEX));
}
}

View File

@@ -0,0 +1,11 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.recipient;
import io.github.domainprimitives.type.ValueObject;
import static io.github.domainprimitives.validation.Constraints.hasMinLength;
public class Name extends ValueObject<String> {
public Name(String value) {
super(value, hasMinLength(3));
}
}

View File

@@ -0,0 +1,12 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.service;
public class LoanAgreementException extends RuntimeException {
public LoanAgreementException(String message) {
super(message);
}
public LoanAgreementException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,43 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.service;
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.LoanAgreementNumber;
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.LoanAgreementDistributor;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementQuery;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.ProcessEngineCommand;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class LoanAgreementService implements LoanAgreementCreation, LoanAgreementStatusCommand {
private final LoanAgreementCommand loanAgreementCommand;
private final ProcessEngineCommand processEngineCommand;
private final LoanAgreementDistributor loanAgreementDistributor;
private final LoanAgreementQuery loanAgreementQuery;
@Override
public void create(LoanAgreement loanAgreement, CaseId caseId) {
try {
LoanAgreementNumber loanAgreementNumber = loanAgreementCommand.save(loanAgreement);
processEngineCommand.startLoanAgreement(caseId, loanAgreementNumber);
} catch (Exception e) {
throw new LoanAgreementException("Cloud not save the loan agreement", e);
}
}
@Override
public void accept(LoanAgreementNumber loanAgreementNumber) {
loanAgreementDistributor.sendLoanAgreement(loanAgreementQuery.loadByNumber(loanAgreementNumber), true);
}
@Override
public void reject(LoanAgreementNumber loanAgreementNumber) {
loanAgreementDistributor.sendLoanAgreement(loanAgreementQuery.loadByNumber(loanAgreementNumber), false);
}
}

View File

@@ -0,0 +1,8 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.CaseId;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
public interface LoanAgreementCreation {
void create(LoanAgreement loanAgreement, CaseId caseId);
}

View File

@@ -0,0 +1,8 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.in;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
public interface LoanAgreementStatusCommand {
void accept(LoanAgreementNumber loanAgreementNumber);
void reject(LoanAgreementNumber loanAgreementNumber);
}

View File

@@ -0,0 +1,8 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
public interface LoanAgreementCommand {
LoanAgreementNumber save(LoanAgreement loanAgreement);
}

View File

@@ -0,0 +1,7 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
public interface LoanAgreementDistributor {
void sendLoanAgreement(LoanAgreement loanAgreement, boolean accepted);
}

View File

@@ -0,0 +1,8 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
public interface LoanAgreementQuery {
LoanAgreement loadByNumber(LoanAgreementNumber loanAgreementNumber);
}

View File

@@ -0,0 +1,8 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.CaseId;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
public interface ProcessEngineCommand {
void startLoanAgreement(CaseId caseId, LoanAgreementNumber loanAgreementNumber);
}

View File

@@ -0,0 +1,12 @@
spring.datasource.url: jdbc:h2:file:./camunda-h2-database
spring:
jpa:
hibernate:
ddl-auto: create-drop
camunda.bpm.admin-user:
id: admin
password: pw
generic-properties:
properties:
initializeTelemetry: false

View File

@@ -0,0 +1,36 @@
<?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">
<decision id="approvement-check" name="Approve Loan Agreement">
<decisionTable id="DecisionTable_0ffqkch">
<input id="Input_1" biodi:width="192" camunda:inputVariable="loanAgreementNumber">
<inputExpression id="InputExpression_1" typeRef="integer">
<text>loanAgreementNumber</text>
</inputExpression>
</input>
<output id="Output_1" name="approved" typeRef="boolean" biodi:width="192" />
<rule id="DecisionRule_0p9ijdl">
<inputEntry id="UnaryTests_1qou5p9">
<text>&gt;= 5</text>
</inputEntry>
<outputEntry id="LiteralExpression_1hlf608">
<text>false</text>
</outputEntry>
</rule>
<rule id="DecisionRule_1arucld">
<inputEntry id="UnaryTests_1vqubek">
<text>&lt; 5</text>
</inputEntry>
<outputEntry id="LiteralExpression_1ovr1wt">
<text>true</text>
</outputEntry>
</rule>
</decisionTable>
</decision>
<dmndi:DMNDI>
<dmndi:DMNDiagram>
<dmndi:DMNShape dmnElementRef="approvement-check">
<dc:Bounds height="80" width="180" x="160" y="100" />
</dmndi:DMNShape>
</dmndi:DMNDiagram>
</dmndi:DMNDI>
</definitions>

View File

@@ -0,0 +1,142 @@
<?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:collaboration id="Collaboration_0w4ikjr">
<bpmn:participant id="Participant_1p6ilgg" name="Loan Agreement example" processRef="Loan_Agreement" />
<bpmn:participant id="Participant_0y932pu" name="Third-party-legacy System" />
<bpmn:participant id="Participant_0z5fkq5" name="Third-party-legacy System" />
<bpmn:messageFlow id="Flow_1ytdopc" sourceRef="RejectLoanAgreementServiceTask" targetRef="Participant_0y932pu" />
<bpmn:messageFlow id="Flow_1ax0a41" sourceRef="ApproveLoanAgreementServiceTask" targetRef="Participant_0z5fkq5" />
</bpmn:collaboration>
<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:extensionElements />
<bpmn:incoming>Flow_1rrsueh</bpmn:incoming>
<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:endEvent id="LoanAgreementNotApprovedEndEvent" name="loan agreement not approved">
<bpmn:incoming>Flow_0eif63m</bpmn:incoming>
</bpmn:endEvent>
<bpmn:endEvent id="LoanAgreementApprovedEndEvent" name="loan agreement approved">
<bpmn:incoming>Flow_1uqmps7</bpmn:incoming>
</bpmn:endEvent>
<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:serviceTask id="RejectLoanAgreementServiceTask" name="Rejection loan agreement" camunda:delegateExpression="${rejectionLoanAgreement}">
<bpmn:incoming>Flow_0xpo6jp</bpmn:incoming>
<bpmn:outgoing>Flow_0eif63m</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:sequenceFlow id="Flow_0eif63m" sourceRef="RejectLoanAgreementServiceTask" targetRef="LoanAgreementNotApprovedEndEvent" />
<bpmn:sequenceFlow id="Flow_1uqmps7" sourceRef="ApproveLoanAgreementServiceTask" targetRef="LoanAgreementApprovedEndEvent" />
<bpmn:sequenceFlow id="Flow_1hri7xc" name="yes" sourceRef="IsApprovedGateway" targetRef="ApproveLoanAgreementServiceTask">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${approved}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_0xpo6jp" name="no" sourceRef="IsApprovedGateway" targetRef="RejectLoanAgreementServiceTask">
<bpmn:conditionExpression xsi:type="bpmn:tFormalExpression">${!approved}</bpmn:conditionExpression>
</bpmn:sequenceFlow>
<bpmn:sequenceFlow id="Flow_00ukhfv" sourceRef="ApproveAgreementRuleTask" targetRef="IsApprovedGateway" />
<bpmn:sequenceFlow id="Flow_1rrsueh" sourceRef="LoanAgreementReciedStartEvent" targetRef="ApproveAgreementRuleTask" />
</bpmn:process>
<bpmn:message id="Message_0o102df" name="loanAgreementReceivedMessage" />
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Collaboration_0w4ikjr">
<bpmndi:BPMNShape id="Participant_1p6ilgg_di" bpmnElement="Participant_1p6ilgg" isHorizontal="true">
<dc:Bounds x="160" y="177" width="720" height="250" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0eif63m_di" bpmnElement="Flow_0eif63m">
<di:waypoint x="700" y="360" />
<di:waypoint x="772" y="360" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1uqmps7_di" bpmnElement="Flow_1uqmps7">
<di:waypoint x="700" y="237" />
<di:waypoint x="772" y="237" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1hri7xc_di" bpmnElement="Flow_1hri7xc">
<di:waypoint x="535" y="237" />
<di:waypoint x="600" y="237" />
<bpmndi:BPMNLabel>
<dc:Bounds x="559" y="219" width="18" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0xpo6jp_di" bpmnElement="Flow_0xpo6jp">
<di:waypoint x="510" y="262" />
<di:waypoint x="510" y="360" />
<di:waypoint x="600" y="360" />
<bpmndi:BPMNLabel>
<dc:Bounds x="519" y="308" width="13" height="14" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_00ukhfv_di" bpmnElement="Flow_00ukhfv">
<di:waypoint x="430" y="237" />
<di:waypoint x="485" y="237" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1rrsueh_di" bpmnElement="Flow_1rrsueh">
<di:waypoint x="258" y="237" />
<di:waypoint x="330" y="237" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNShape id="Activity_1244zx8_di" bpmnElement="ApproveAgreementRuleTask">
<dc:Bounds x="330" y="197" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Gateway_0xmpbgn_di" bpmnElement="IsApprovedGateway" isMarkerVisible="true">
<dc:Bounds x="485" y="212" width="50" height="50" />
<bpmndi:BPMNLabel>
<dc:Bounds x="478" y="182" width="65" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0r30zqn_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="Event_1csw1p9_di" bpmnElement="LoanAgreementApprovedEndEvent">
<dc:Bounds x="772" y="219" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="752" y="262" width="77" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_13yt0mp_di" bpmnElement="ApproveLoanAgreementServiceTask">
<dc:Bounds x="600" y="197" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1uvc9z4_di" bpmnElement="RejectLoanAgreementServiceTask">
<dc:Bounds x="600" y="320" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_086ty8n_di" bpmnElement="LoanAgreementReciedStartEvent">
<dc:Bounds x="222" y="219" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="202" y="262" width="77" height="27" />
</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: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:BPMNEdge id="Flow_1ax0a41_di" bpmnElement="Flow_1ax0a41">
<di:waypoint x="650" y="197" />
<di:waypoint x="650" y="140" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1ytdopc_di" bpmnElement="Flow_1ytdopc">
<di:waypoint x="650" y="400" />
<di:waypoint x="650" y="460" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@@ -0,0 +1,115 @@
package de.weinbrecht.luc.bpm.architecture;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.process.ApproveLoanAgreement;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.process.RejectionLoanAgreement;
import org.camunda.bpm.dmn.engine.DmnDecisionTableResult;
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.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 java.util.stream.Stream;
import static de.weinbrecht.luc.bpm.architecture.common.ProcessConstants.LOAN_AGREEMENT_NUMBER;
import static de.weinbrecht.luc.bpm.architecture.common.ProcessConstants.PROCESS_DEFINITION;
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 {
private static final String START_EVENT = "LoanAgreementReciedStartEvent";
private static final String APPROVE_RULE_TASK = "ApproveAgreementRuleTask";
private static final String APPROVE_AGREEMENT_SERVICE_TASK = "ApproveLoanAgreementServiceTask";
private static final String REJECT_AGREEMENT_SERVICE_TASK = "RejectLoanAgreementServiceTask";
private static final String DMN_DEFINITION = "approvement-check";
@BeforeEach
void setUp() {
registerJavaDelegateMock(ApproveLoanAgreement.class);
registerJavaDelegateMock(RejectionLoanAgreement.class);
}
@Test
@Deployment(resources = { "loan_agreement.bpmn", "approve_agreement.dmn"})
void shouldExecuteProcess_happy_path() {
ProcessInstance processInstance = runtimeService().startProcessInstanceByKey(
PROCESS_DEFINITION,
withVariables(LOAN_AGREEMENT_NUMBER, 1L)
);
assertThat(processInstance).isActive();
assertThat(processInstance).isWaitingAt(START_EVENT);
execute(job());
assertThat(processInstance)
.hasPassed(START_EVENT,
APPROVE_RULE_TASK,
APPROVE_AGREEMENT_SERVICE_TASK);
verifyJavaDelegateMock(ApproveLoanAgreement.class).executed();
assertThat(processInstance).isEnded();
}
@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);
verifyJavaDelegateMock(RejectionLoanAgreement.class).executed();
assertThat(processInstance).isEnded();
}
@ParameterizedTest
@MethodSource("provideProcessVariablesForDMN")
@Deployment(resources = "approve_agreement.dmn")
void testTweetApprovalIBM(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,83 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement;
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.LoanAgreementNumber;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.service.LoanAgreementException;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.service.LoanAgreementService;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementCommand;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementDistributor;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.LoanAgreementQuery;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.usecase.out.ProcessEngineCommand;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.TestdataGenerator.createLoanAgreementWithNumber;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.*;
@MockitoSettings
class LoanAgreementServiceTest {
@InjectMocks
private LoanAgreementService classUnderTest;
@Mock
private LoanAgreementCommand loanAgreementCommand;
@Mock
private ProcessEngineCommand processEngineCommand;
@Mock
private LoanAgreementDistributor loanAgreementDistributor;
@Mock
private LoanAgreementQuery loanAgreementQuery;
@Test
void should_safe_and_set_loan_number_on_creation() {
LoanAgreement loanAgreement = createLoanAgreementWithNumber();
when(loanAgreementCommand.save(loanAgreement)).thenReturn(loanAgreement.getLoanAgreementNumber());
CaseId caseId = new CaseId("12");
classUnderTest.create(loanAgreement, caseId);
verify(processEngineCommand).startLoanAgreement(caseId, loanAgreement.getLoanAgreementNumber());
}
@Test
void should_catch_exception_and_throw_custom_one_on_creation() {
LoanAgreement loanAgreement = createLoanAgreementWithNumber();
CaseId caseId = new CaseId("12");
when(loanAgreementCommand.save(loanAgreement)).thenThrow(RuntimeException.class);
assertThrows(LoanAgreementException.class,
() -> classUnderTest.create(loanAgreement, caseId));
verify(processEngineCommand, never()).startLoanAgreement(caseId, loanAgreement.getLoanAgreementNumber());
}
@Test
void should_call_distributor_and_set_status_accepted() {
LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L);
LoanAgreement loanAgreement = createLoanAgreementWithNumber();
when(loanAgreementQuery.loadByNumber(loanAgreementNumber)).thenReturn(loanAgreement);
classUnderTest.accept(loanAgreementNumber);
verify(loanAgreementDistributor).sendLoanAgreement(loanAgreement, true);
}
@Test
void should_call_distributor_and_set_status_reject() {
LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L);
LoanAgreement loanAgreement = createLoanAgreementWithNumber();
when(loanAgreementQuery.loadByNumber(loanAgreementNumber)).thenReturn(loanAgreement);
classUnderTest.reject(loanAgreementNumber);
verify(loanAgreementDistributor).sendLoanAgreement(loanAgreement, false);
}
}

View File

@@ -0,0 +1,32 @@
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.usecase.in.LoanAgreementStatusCommand;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings;
import static de.weinbrecht.luc.bpm.architecture.common.ProcessConstants.LOAN_AGREEMENT_NUMBER;
import static org.mockito.Mockito.*;
@MockitoSettings
class ApproveLoanAgreementTest {
@InjectMocks
private ApproveLoanAgreement classUnderTest;
@Mock
private LoanAgreementStatusCommand loanAgreementStatusCommand;
@Test
void should_call_command_and_reject() {
LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L);
DelegateExecution delegateExecution = mock(DelegateExecution.class);
when(delegateExecution.getVariable(LOAN_AGREEMENT_NUMBER)).thenReturn(loanAgreementNumber.getValue());
classUnderTest.execute(delegateExecution);
verify(loanAgreementStatusCommand).accept(loanAgreementNumber);
}
}

View File

@@ -0,0 +1,33 @@
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.usecase.in.LoanAgreementStatusCommand;
import org.camunda.bpm.engine.delegate.DelegateExecution;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings;
import static de.weinbrecht.luc.bpm.architecture.common.ProcessConstants.LOAN_AGREEMENT_NUMBER;
import static org.mockito.Mockito.*;
@MockitoSettings
class RejectionLoanAgreementTest {
@InjectMocks
private RejectionLoanAgreement classUnderTest;
@Mock
private LoanAgreementStatusCommand loanAgreementStatusCommand;
@Test
void should_call_command_and_reject() {
LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L);
DelegateExecution delegateExecution = mock(DelegateExecution.class);
when(delegateExecution.getVariable(LOAN_AGREEMENT_NUMBER)).thenReturn(loanAgreementNumber.getValue());
classUnderTest.execute(delegateExecution);
verify(loanAgreementStatusCommand).reject(loanAgreementNumber);
}
}

View File

@@ -0,0 +1,52 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.web;
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.usecase.in.LoanAgreementCreation;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON_VALUE;
@WebMvcTest(LoanAgreementController.class)
@Import(LoanAgreementMapper.class)
class LoanAgreementControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private LoanAgreementCreation loanAgreementCreation;
@Test
void should_class_creation_on_post() throws Exception {
String requestJson = "{\"customerNumber\": \"A1\",\"name\": \"Tester\",\"mailAddress\": \"tester@web.io\",\"amount\": 1100}";
mockMvc.perform(
post("/loan/agreement/1")
.contentType(APPLICATION_JSON_VALUE)
.content(requestJson)
)
.andDo(print())
.andExpect(status().isOk());
verify(loanAgreementCreation).create(
new LoanAgreement(
new Recipient(
new CustomerNumber("A1"),
new Name("Tester"),
new MailAddress("tester@web.io")
),
new Amount(1100)
),
new CaseId("1"));
}
}

View File

@@ -0,0 +1,33 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.in.web;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class LoanAgreementMapperTest {
private LoanAgreementMapper classUnderTest = new LoanAgreementMapper();
@Test
void should_map_all_fields() {
LoanAgreementResource loanAgreementResource = createLoanAgreementResource();
LoanAgreement result = classUnderTest.mapToDomain(loanAgreementResource);
assertThat(result).isNotNull();
assertThat(result.getAmount().getValue()).isEqualTo(loanAgreementResource.getAmount());
assertThat(result.getRecipient().getName().getValue()).isEqualTo(loanAgreementResource.getName());
assertThat(result.getRecipient().getMailAddress().getValue()).isEqualTo(loanAgreementResource.getMailAddress());
assertThat(result.getRecipient().getCustomerNumber().getValue()).isEqualTo(loanAgreementResource.getCustomerNumber());
}
private LoanAgreementResource createLoanAgreementResource() {
LoanAgreementResource loanAgreementResource = new LoanAgreementResource();
loanAgreementResource.setAmount(400);
loanAgreementResource.setCustomerNumber("A11");
loanAgreementResource.setMailAddress("tester@web.io");
loanAgreementResource.setName("Tester");
return loanAgreementResource;
}
}

View File

@@ -0,0 +1,22 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.db;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import static org.assertj.core.api.Assertions.assertThat;
@DataJpaTest
class LoanAgreementCRUDRepositoryTest {
@Autowired
private LoanAgreementCRUDRepository classUnderTest;
@Test
void should_safe_entity() {
LoanAgreementEntity result = classUnderTest.save(new LoanAgreementEntity());
assertThat(result).isNotNull();
assertThat(result.getId()).isNotNull();
}
}

View File

@@ -0,0 +1,38 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.db;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
import org.junit.jupiter.api.Test;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.db.TestDataGenerator.createLoanAgreementEntity;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.TestdataGenerator.createLoanAgreement;
import static org.assertj.core.api.Assertions.assertThat;
class LoanAgreementMapperTest {
private LoanAgreementMapper classUnderTest = new LoanAgreementMapper();
@Test
void should_map_all_fields_to_db() {
LoanAgreement loanAgreement = createLoanAgreement();
LoanAgreementEntity result = classUnderTest.mapToDb(loanAgreement);
assertThat(result.getAmount()).isEqualTo(loanAgreement.getAmount().getValue());
assertThat(result.getName()).isEqualTo(loanAgreement.getRecipient().getName().getValue());
assertThat(result.getMailAddress()).isEqualTo(loanAgreement.getRecipient().getMailAddress().getValue());
assertThat(result.getCustomerNumber()).isEqualTo(loanAgreement.getRecipient().getCustomerNumber().getValue());
}
@Test
void should_map_all_fields_to_domain() {
LoanAgreementEntity loanAgreementEntity = createLoanAgreementEntity();
LoanAgreement result = classUnderTest.mapToDomain(loanAgreementEntity);
assertThat(result.getLoanAgreementNumber().getValue()).isEqualTo(loanAgreementEntity.getId());
assertThat(result.getAmount().getValue()).isEqualTo(loanAgreementEntity.getAmount());
assertThat(result.getRecipient().getName().getValue()).isEqualTo(loanAgreementEntity.getName());
assertThat(result.getRecipient().getMailAddress().getValue()).isEqualTo(loanAgreementEntity.getMailAddress());
assertThat(result.getRecipient().getCustomerNumber().getValue()).isEqualTo(loanAgreementEntity.getCustomerNumber());
}
}

View File

@@ -0,0 +1,63 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.db;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreementNumber;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.db.TestDataGenerator.createLoanAgreementEntity;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.TestdataGenerator.createLoanAgreement;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.TestdataGenerator.createLoanAgreementWithNumber;
import static java.util.Optional.empty;
import static java.util.Optional.of;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Answers.CALLS_REAL_METHODS;
import static org.mockito.Mockito.when;
@MockitoSettings
class LoanAgreementRepositoryTest {
@InjectMocks
private LoanAgreementRepository classUnderTest;
@Mock
private LoanAgreementCRUDRepository crudRepository;
@Mock(answer = CALLS_REAL_METHODS)
private LoanAgreementMapper mapper;
@Test
void should_call_crud_repo_and_safe() {
LoanAgreement loanAgreement = createLoanAgreement();
LoanAgreementEntity dbEntity = createLoanAgreementEntity();
when(crudRepository.save(mapper.mapToDb(loanAgreement))).thenReturn(dbEntity);
LoanAgreementNumber result = classUnderTest.save(loanAgreement);
assertThat(result.getValue()).isEqualTo(dbEntity.getId());
}
@Test
void should_call_crud_repo_and_find_by_id() {
LoanAgreement loanAgreement = createLoanAgreementWithNumber();
when(crudRepository.findById(loanAgreement.getLoanAgreementNumber().getValue()))
.thenReturn(of(createLoanAgreementEntity()));
LoanAgreement result = classUnderTest.loadByNumber(loanAgreement.getLoanAgreementNumber());
assertThat(result).isEqualTo(loanAgreement);
}
@Test
void should_call_crud_repo_and_find_by_id_throw_custom_exception_if_not_found() {
LoanAgreement loanAgreement = createLoanAgreementWithNumber();
when(crudRepository.findById(loanAgreement.getLoanAgreementNumber().getValue())).thenReturn(empty());
assertThrows(LoanAgreementNotFoundException.class,
() -> classUnderTest.loadByNumber(loanAgreement.getLoanAgreementNumber()));
}
}

View File

@@ -0,0 +1,18 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.adapter.out.db;
import de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.LoanAgreement;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.TestdataGenerator.createLoanAgreementWithNumber;
class TestDataGenerator {
public static LoanAgreementEntity createLoanAgreementEntity() {
LoanAgreement loanAgreement = createLoanAgreementWithNumber();
LoanAgreementEntity loanAgreementEntity = new LoanAgreementEntity();
loanAgreementEntity.setId(loanAgreement.getLoanAgreementNumber().getValue());
loanAgreementEntity.setAmount(loanAgreement.getAmount().getValue());
loanAgreementEntity.setName(loanAgreement.getRecipient().getName().getValue());
loanAgreementEntity.setCustomerNumber(loanAgreement.getRecipient().getCustomerNumber().getValue());
loanAgreementEntity.setMailAddress(loanAgreement.getRecipient().getMailAddress().getValue());
return loanAgreementEntity;
}
}

View File

@@ -0,0 +1,39 @@
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.LoanAgreementNumber;
import org.camunda.bpm.engine.RuntimeService;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoSettings;
import java.util.HashMap;
import java.util.Map;
import static de.weinbrecht.luc.bpm.architecture.common.ProcessConstants.LOAN_AGREEMENT_NUMBER;
import static de.weinbrecht.luc.bpm.architecture.common.ProcessConstants.START_EVENT_MESSAGE_REF;
import static org.mockito.Mockito.verify;
@MockitoSettings
class ProcessEngineClientTest {
@InjectMocks
private ProcessEngineClient classUnderTest;
@Mock
private RuntimeService runtimeService;
private final CaseId caseId = new CaseId("11");
private final LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L);
@Test
void should_class_runtime_service_to_start() {
Map<String, Object> processVariables = new HashMap<>();
processVariables.put(LOAN_AGREEMENT_NUMBER, loanAgreementNumber.getValue());
classUnderTest.startLoanAgreement(caseId, loanAgreementNumber);
verify(runtimeService).startProcessInstanceByMessage(START_EVENT_MESSAGE_REF, caseId.getValue(), processVariables);
}
}

View File

@@ -0,0 +1,72 @@
package de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model;
import io.github.domainprimitives.validation.InvariantException;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static de.weinbrecht.luc.bpm.architecture.loan.agreement.domain.model.TestdataGenerator.createRecipient;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
class LoanAgreementTest {
@Test
void should_create_valid_object() {
Recipient recipient = createRecipient();
Amount amount = new Amount(300);
LoanAgreement loanAgreement = new LoanAgreement(recipient, amount);
assertThat(loanAgreement).isNotNull();
assertThat(loanAgreement.getRecipient()).isEqualTo(recipient);
assertThat(loanAgreement.getAmount()).isEqualTo(amount);
}
@Test
void should_create_valid_object_with_numer() {
Recipient recipient = createRecipient();
Amount amount = new Amount(300);
LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L);
LoanAgreement loanAgreement = new LoanAgreement(loanAgreementNumber, recipient, amount);
assertThat(loanAgreement).isNotNull();
assertThat(loanAgreement.getRecipient()).isEqualTo(recipient);
assertThat(loanAgreement.getAmount()).isEqualTo(amount);
assertThat(loanAgreement.getLoanAgreementNumber()).isEqualTo(loanAgreementNumber);
}
@Test
void should_create_valid_object_with_number() {
Recipient recipient = createRecipient();
Amount amount = new Amount(300);
LoanAgreementNumber loanAgreementNumber = new LoanAgreementNumber(1L);
LoanAgreement loanAgreement = new LoanAgreement(loanAgreementNumber, recipient, amount);
assertNotNull(loanAgreement);
assertEquals(loanAgreement.getLoanAgreementNumber(), loanAgreementNumber);
assertEquals(loanAgreement.getRecipient(), recipient);
assertEquals(loanAgreement.getAmount(), amount);
}
@Nested
class InvariantTest {
@Test
void should_throw_invariant_exception_if_recipient_is_null() {
Amount amount = new Amount(300);
assertThrows(InvariantException.class, () -> new LoanAgreement(null, amount));
}
@Test
void should_throw_invariant_exception_if_amount_is_null() {
assertThrows(InvariantException.class, () -> new LoanAgreement(createRecipient(), null));
}
@Test
void should_throw_invariant_exception_if_number_is_null() {
Amount amount = new Amount(300);
assertThrows(InvariantException.class, () -> new LoanAgreement(null, createRecipient(), amount));
}
}
}

View File

@@ -0,0 +1,54 @@
package 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 io.github.domainprimitives.validation.InvariantException;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
class RecipientTest {
@Test
void should_create_valid_object() {
Name name = new Name("Tester");
MailAddress mailAddress = new MailAddress("tester@web.io");
CustomerNumber customerNumber = new CustomerNumber("A-1");
Recipient recipient = new Recipient(customerNumber, name, mailAddress);
assertThat(recipient).isNotNull();
assertThat(recipient.getCustomerNumber()).isEqualTo(customerNumber);
assertThat(recipient.getName()).isEqualTo(name);
assertThat(recipient.getMailAddress()).isEqualTo(mailAddress);
}
@Nested
class InvariantTest {
@Test
void should_throw_invariant_exception_if_customer_number_is_null() {
Name name = new Name("Tester");
MailAddress mailAddress = new MailAddress("tester@web.io");
assertThrows(InvariantException.class, () -> new Recipient(null, name, mailAddress));
}
@Test
void should_throw_invariant_exception_if_name_is_null() {
MailAddress mailAddress = new MailAddress("tester@web.io");
CustomerNumber customerNumber = new CustomerNumber("A-1");
assertThrows(InvariantException.class, () -> new Recipient(customerNumber, null, mailAddress));
}
@Test
void should_throw_invariant_exception_if_mail_is_null() {
Name name = new Name("Tester");
CustomerNumber customerNumber = new CustomerNumber("A-1");
assertThrows(InvariantException.class, () -> new Recipient(customerNumber, name, null));
}
}
}

View File

@@ -0,0 +1,35 @@
package 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;
public class TestdataGenerator {
public static Recipient createRecipient() {
return new Recipient(new CustomerNumber("A-1"), new Name("Tester"), new MailAddress("tester@newweb.io"));
}
public static LoanAgreement createLoanAgreement() {
return new LoanAgreement(
new LoanAgreementNumber(1L),
new Recipient(
new CustomerNumber("A-1"),
new Name("Tester"),
new MailAddress("tester@web.de")
),
new Amount(400)
);
}
public static LoanAgreement createLoanAgreementWithNumber() {
return new LoanAgreement(
new LoanAgreementNumber(1L),
new Recipient(
new CustomerNumber("A-1"),
new Name("Tester"),
new MailAddress("tester@web.de")
),
new Amount(400)
);
}
}

View File

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

View File

@@ -0,0 +1,22 @@
<?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

@@ -0,0 +1,63 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.apache.ibatis" level="info" />
<!--
<logger name="org.apache.ibatis" level="DEBUG"/>
-->
<logger name="javax.activation" level="info" />
<logger name="org.springframework" level="info" />
<logger name="org.camunda" level="info" />
<!--
<logger name="org.camunda" level="DEBUG"/>
-->
<logger name="org.camunda.bpm.engine.test" level="debug" />
<!--
<logger name="org.camunda.bpm.engine.bpmn.parser" level="debug" />
<logger name="org.camunda.bpm.engine.bpmn.behavior" level="debug" />
<logger name="org.camunda.bpm.engine.cmmn.transformer" level="debug" />
<logger name="org.camunda.bpm.engine.cmmn.behavior" level="debug" />
<logger name="org.camunda.bpm.engine.cmmn.operation" level="debug" />
<logger name="org.camunda.bpm.engine.cmd" level="debug" />
<logger name="org.camunda.bpm.engine.persistence" level="debug" />
<logger name="org.camunda.bpm.engine.impl.persistence.entity" level="debug" />
<logger name="org.camunda.bpm.engine.impl.history.event" level="debug" />
<logger name="org.camunda.bpm.engine.impl.batch.history" level="debug" />
<logger name="org.camunda.bpm.engine.impl.batch" level="debug" />
<logger name="org.camunda.bpm.engine.history" level="debug" />
<logger name="org.camunda.bpm.engine.impl.cmmn.entity.repository" level="debug" />
<logger name="org.camunda.bpm.engine.impl.cmmn.entity.runtime" level="debug" />
<logger name="org.camunda.bpm.engine.impl.dmn.entity.repository" level="debug" />
<logger name="org.camunda.bpm.engine.tx" level="debug" />
<logger name="org.camunda.bpm.engine.cfg" level="debug" />
<logger name="org.camunda.bpm.engine.jobexecutor" level="debug" />
<logger name="org.camunda.bpm.engine.context" level="debug" />
<logger name="org.camunda.bpm.engine.core" level="debug" />
<logger name="org.camunda.bpm.engine.pvm" level="debug" />
<logger name="org.camunda.bpm.engine.metrics" level="debug" />
<logger name="org.camunda.bpm.engine.util" level="debug" />
<logger name="org.camunda.bpm.application" level="debug" />
<logger name="org.camunda.bpm.container" level="debug" />
-->
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>