diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 0ec06fa..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: CI - -on: [push] - -jobs: - build: - runs-on: ubuntu-latest - steps: - - - name: "Checkout sources" - uses: actions/checkout@v1 - - - name: "Setup Java" - uses: actions/setup-java@v1 - with: - java-version: 17 - - - name: "Initialize Gradle dependencies cache" - uses: actions/cache@v2 - with: - path: ~/.gradle/caches - key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/build.gradle') }} - - - name: "Run Gradle build" - run: chmod 755 gradlew && ./gradlew build - - - name: "Zip build reports" - if: failure() - run: zip -r reports.zip build/reports - - - uses: actions/upload-artifact@v1 - name: "Upload build reports" - if: failure() - with: - name: reports - path: reports.zip diff --git a/README.md b/README.md index cf27303..6d19ed3 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,48 @@ -# Get Your Hands Dirty on Clean Architecture +# Spring Boot Hexagonal +Project shows, how to use hexagonal architecture in your spring boot applications -This repository implements a small web app in the Hexagonal Architecture style, as discussed in the book "Get Your Hands Dirty on Clean Architecture". - -The code has been updated to the 2nd edition of the book. - -## Get the print book - -[![Get Your Hands Dirty on Clean Architecture cover](img/cover-packt-450.png)](https://www.amazon.com/Your-Hands-Dirty-Clean-Architecture/dp/180512837X?keywords=get+your+hands+dirty+on+clean+architecture&qid=1689324075&sprefix=Get+Your+Hands+Dirty+on+Clean+,aps,424&sr=8-2&_encoding=UTF8&tag=reflectorin0c-20&linkCode=ur2&linkId=c04a12e6dd6d399747b0cdce328650a5&camp=1789&creative=9325) - -## Get the e-book - -This is the self-published version, which is only available electronically. +![Hexagonal Architecture](img/hexagonal-architecture.png) -[![Get Your Hands Dirty on Clean Architecture cover](img/cover-430.png)](https://thombergs.gumroad.com/l/gyhdoca) -## Companion Articles +## Implemented integrations: +* Mysql (Spring Data JPA) +* Redis (Spring Data Redis) -* [Hexagonal Architecture with Java and Spring](https://reflectoring.io/spring-hexagonal/) -* [Building a Multi-Module Spring Boot Application with Gradle](https://reflectoring.io/spring-boot-gradle-multi-module/) ## Prerequisites * JDK 17 * this project uses Lombok, so enable annotation processing in your IDE - -## About the book -### All About Hexagonal Architecture - -* Learn the concepts behind "Clean Architecture" and "Hexagonal Architecture". -* Explore a hands-on approach of implementing a Hexagonal architecture with example code [on GitHub](https://github.com/thombergs/buckpal). -* Develop your domain code independent of database or web concerns. - -![Hexagonal Architecture](img/hexagonal-architecture.png) - -### Get a Grip on Your Layers - -* Learn about potential problems of the common layered architecture style. -* Free your domain layer of oppressive dependencies using dependency inversion. -* Structure your code in an architecturally expressive way. -* Use different methods to enforce architectural boundaries. -* Learn the consequences of shortcuts and when to accept them. -* ... and [more](#table-of-contents). - -![Dependencies](img/dependencies.png) - -### What Readers Say - -> Tom Hombergs has done a terrific job in explaining clean architecture - from concepts to code. Really wish more technical books would be as clear as that one! - -Gernot Starke - Fellow at [INNOQ](https://www.innoq.com/en/staff/gernot-starke/), Founder of [arc42](https://arc42.org/), Author of Software Architecture Books, Coach, and Consultant - -> Love your book. One of the most practical books on hexagonal architecture I have seen/read so far. - -Marten Deinum - Spring Framework Contributor and Author of ["Spring 5 Recipes"](https://www.amazon.com/Spring-5-Recipes-Problem-Solution-Approach/dp/1484227891&tag=reflectorin0c-20) and ["Spring Boot 2 Recipes"](https://www.amazon.com/Spring-Boot-Recipes-Problem-Solution-Approach/dp/1484239628&tag=reflectorin0c-20) - -> A book taken right out of the machine room of software development. Tom talks straight from his experience and guides you through the day-to-day trade-offs necessary to deliver clean architecture. - -Sebastian Kempken - Software Architect at Adcubum - -> Thank you for the great book, it helped me gain significant insight into how one would go about implementing hexagonal and DDD in a modern Spring project. - -Spyros Vallianos - Java Developer at Konnekt-able - -> After reading it I had one of these 'aha' moments when things finally click in your brain. - -Manos Tzagkarakis - Java Developer at Datawise - -### Table of Contents - -1. Maintainability -2. What's Wrong with Layers? -3. Inverting Dependencies -4. Organizing Code -5. Implementing a Use Case -6. Implementing a Web Adapter -7. Implementing a Persistence Adapter -8. Testing Architecture Elements -9. Mapping Between Boundaries -10. Assembling the Application -11. Taking Shortcuts Consciously -12. Enforcing Architecture Boundaries -13. Managing Multiple Bounded Contexts -14. A Component-Based Approach to Software Architecture -15. Deciding on an Architecture Style +* this project uses Testcontainers, so run Docker on your local machine + +## Getting Started +`gradle testBootRun` + +## Project Structure +``` +└──com/ + └── yourcompany/ + ├── adapter/ # Adapter logic + │ ├── in/ # Incoming requests adapters + │ │ └── http/ + │ └── out/ # Outgoing requests adapters + │ ├── cache/ + │ ├── kafka/ + │ └── persistense/ + ├── application # Core logic + │ ├── domain/ + │ │ ├── model/ + │ │ └── service/ + │ └── port/ # Core logic API + │ ├── in/ + │ └── out/ + └── common/ # Neither business logic nor adapters +``` + +## See More + +* [Гексагональная Архитектура и Spring Boot](https://habr.com/ru/articles/795127/) +* Forked and inspired by [hombergs/buckpal](https://github.com/thombergs/buckpal) +* [YouTube: Рустам Ахметов — Архитектура приложения и ошибки проектирования](https://www.youtube.com/watch?v=X6QdWTE1HHw&t=2194s&ab_channel=JPoint%2CJoker%D0%B8JUGru) +* [Hexagonal Architecture with Java and Spring](https://reflectoring.io/spring-hexagonal/) +* [Building a Multi-Module Spring Boot Application with Gradle](https://reflectoring.io/spring-boot-gradle-multi-module/) diff --git a/application/build.gradle b/application/build.gradle new file mode 100644 index 0000000..8732ddf --- /dev/null +++ b/application/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'core.java-conventions' +} diff --git a/application/src/main/java/module-info.java b/application/src/main/java/module-info.java new file mode 100644 index 0000000..ef69dcf --- /dev/null +++ b/application/src/main/java/module-info.java @@ -0,0 +1,7 @@ +module spring.boot.hexagonal.application { + exports nd.jar.springhexboot.application.port.in; + exports nd.jar.springhexboot.application.domain.model; + exports nd.jar.springhexboot.application.port.out; + requires static lombok; + requires spring.context; +} \ No newline at end of file diff --git a/application/src/main/java/nd/jar/springhexboot/application/domain/model/Event.java b/application/src/main/java/nd/jar/springhexboot/application/domain/model/Event.java new file mode 100644 index 0000000..90d672b --- /dev/null +++ b/application/src/main/java/nd/jar/springhexboot/application/domain/model/Event.java @@ -0,0 +1,8 @@ +package nd.jar.springhexboot.application.domain.model; + +public record Event( + String id, + String name, + String description, + String from +){} \ No newline at end of file diff --git a/application/src/main/java/nd/jar/springhexboot/application/domain/service/EventService.java b/application/src/main/java/nd/jar/springhexboot/application/domain/service/EventService.java new file mode 100644 index 0000000..d714ba9 --- /dev/null +++ b/application/src/main/java/nd/jar/springhexboot/application/domain/service/EventService.java @@ -0,0 +1,32 @@ +package nd.jar.springhexboot.application.domain.service; + +import lombok.RequiredArgsConstructor; +import nd.jar.springhexboot.application.domain.model.Event; +import nd.jar.springhexboot.application.port.in.FindEventsUseCase; +import nd.jar.springhexboot.application.port.in.PushEventUseCase; +import nd.jar.springhexboot.application.port.out.ExternalStorage; +import org.springframework.stereotype.Service; + +import java.util.Map; + +import static java.util.stream.Collectors.toMap; + +@Service +@RequiredArgsConstructor +public class EventService implements PushEventUseCase, FindEventsUseCase { + private final Map storages; + @Override + public boolean push(Event event) { + return storages.values().stream().map(sub -> sub.push(event)) + .anyMatch(result -> !result); + } + + @Override + public Map find(String eventId) { + return storages.entrySet().stream() + .collect(toMap(Map.Entry::getKey, entry -> entry.getValue().find(eventId))).entrySet().stream() + .filter(entry -> entry.getValue().isPresent()) + .collect(toMap(Map.Entry::getKey, entry -> entry.getValue().get())); + } + +} diff --git a/application/src/main/java/nd/jar/springhexboot/application/port/in/FindEventsUseCase.java b/application/src/main/java/nd/jar/springhexboot/application/port/in/FindEventsUseCase.java new file mode 100644 index 0000000..c23d3ac --- /dev/null +++ b/application/src/main/java/nd/jar/springhexboot/application/port/in/FindEventsUseCase.java @@ -0,0 +1,9 @@ +package nd.jar.springhexboot.application.port.in; + +import nd.jar.springhexboot.application.domain.model.Event; + +import java.util.Map; + +public interface FindEventsUseCase { + Map find(String eventId); +} diff --git a/application/src/main/java/nd/jar/springhexboot/application/port/in/PushEventUseCase.java b/application/src/main/java/nd/jar/springhexboot/application/port/in/PushEventUseCase.java new file mode 100644 index 0000000..2eeb53c --- /dev/null +++ b/application/src/main/java/nd/jar/springhexboot/application/port/in/PushEventUseCase.java @@ -0,0 +1,7 @@ +package nd.jar.springhexboot.application.port.in; + +import nd.jar.springhexboot.application.domain.model.Event; + +public interface PushEventUseCase { + boolean push(Event event); +} diff --git a/application/src/main/java/nd/jar/springhexboot/application/port/out/ExternalStorage.java b/application/src/main/java/nd/jar/springhexboot/application/port/out/ExternalStorage.java new file mode 100644 index 0000000..3976d4a --- /dev/null +++ b/application/src/main/java/nd/jar/springhexboot/application/port/out/ExternalStorage.java @@ -0,0 +1,10 @@ +package nd.jar.springhexboot.application.port.out; + +import nd.jar.springhexboot.application.domain.model.Event; + +import java.util.Optional; + +public interface ExternalStorage { + boolean push(Event event); + Optional find(String id); +} diff --git a/application/src/main/java/nd/jar/springhexboot/application/port/out/GetEventPort.java b/application/src/main/java/nd/jar/springhexboot/application/port/out/GetEventPort.java new file mode 100644 index 0000000..8212464 --- /dev/null +++ b/application/src/main/java/nd/jar/springhexboot/application/port/out/GetEventPort.java @@ -0,0 +1,9 @@ +package nd.jar.springhexboot.application.port.out; + +import nd.jar.springhexboot.application.domain.model.Event; + +import java.util.Optional; + +public interface GetEventPort { + Optional find(String id); +} diff --git a/application/src/main/java/nd/jar/springhexboot/application/port/out/GetEventsPort.java b/application/src/main/java/nd/jar/springhexboot/application/port/out/GetEventsPort.java new file mode 100644 index 0000000..ed3bf44 --- /dev/null +++ b/application/src/main/java/nd/jar/springhexboot/application/port/out/GetEventsPort.java @@ -0,0 +1,9 @@ +package nd.jar.springhexboot.application.port.out; + +import nd.jar.springhexboot.application.domain.model.Event; + +import java.util.List; + +public interface GetEventsPort { + List getAll(); +} diff --git a/bootstrap/build.gradle b/bootstrap/build.gradle new file mode 100644 index 0000000..8d50be2 --- /dev/null +++ b/bootstrap/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'core.java-conventions' + id 'org.springframework.boot' version '3.2.5' + id 'application' +} +application { + mainClass = 'nd.jar.springhexboot.App' +} + +dependencies { + implementation project(":application") + implementation project(":out.persistence") + implementation project(":in.http") + implementation project(":out.redis") + implementation project(":out.kafka") + + implementation 'org.springframework.boot:spring-boot-autoconfigure' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-testcontainers' + testImplementation "org.springframework.boot:spring-boot-devtools" + testImplementation "org.testcontainers:mysql:1.19.0" + testImplementation "org.testcontainers:kafka:1.19.5" + testImplementation "com.redis:testcontainers-redis:2.0.1" +} \ No newline at end of file diff --git a/bootstrap/src/main/java/module-info.java b/bootstrap/src/main/java/module-info.java new file mode 100644 index 0000000..5b20375 --- /dev/null +++ b/bootstrap/src/main/java/module-info.java @@ -0,0 +1,12 @@ +module spring.boot.hexagonal.bootstrap { + exports nd.jar.springhexboot; + requires spring.boot.hexagonal.application; + requires spring.boot.hexagonal.in.http; + requires spring.boot.hexagonal.out.persistence; + requires spring.boot.autoconfigure; + requires spring.boot; + requires spring.boot.hexagonal.out.redis; + requires spring.boot.hexagonal.out.kafka; + + opens nd.jar.springhexboot to spring.core; +} \ No newline at end of file diff --git a/bootstrap/src/main/java/nd/jar/springhexboot/App.java b/bootstrap/src/main/java/nd/jar/springhexboot/App.java new file mode 100644 index 0000000..a68e4f4 --- /dev/null +++ b/bootstrap/src/main/java/nd/jar/springhexboot/App.java @@ -0,0 +1,11 @@ +package nd.jar.springhexboot; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class App { + public static void main(String[] args) { + SpringApplication.run(App.class, args); + } +} diff --git a/bootstrap/src/main/resources/application.yml b/bootstrap/src/main/resources/application.yml new file mode 100644 index 0000000..ae3ed1d --- /dev/null +++ b/bootstrap/src/main/resources/application.yml @@ -0,0 +1,20 @@ +spring: + jpa: + hibernate: + ddl-auto: create + show-sql: true +logging: + level: + org.springframework.web.reactive.function.client.ExchangeFunctions: debug + org: + hibernate: + internal: + SessionImpl: DEBUG + SQL: INFO + type: TRACE + loader: + hql: TRACE + engine: + transaction: + internal: + TransactionImpl: DEBUG diff --git a/bootstrap/src/test/java/nd/jar/springhexboot/TestApp.java b/bootstrap/src/test/java/nd/jar/springhexboot/TestApp.java new file mode 100644 index 0000000..435c7f3 --- /dev/null +++ b/bootstrap/src/test/java/nd/jar/springhexboot/TestApp.java @@ -0,0 +1,11 @@ +package nd.jar.springhexboot; + +import org.springframework.boot.SpringApplication; + +public class TestApp { + public static void main(String[] args) { + SpringApplication.from(App::main) + .with(TestContainersConfiguration.class) + .run(); + } +} diff --git a/bootstrap/src/test/java/nd/jar/springhexboot/TestContainersConfiguration.java b/bootstrap/src/test/java/nd/jar/springhexboot/TestContainersConfiguration.java new file mode 100644 index 0000000..65f21aa --- /dev/null +++ b/bootstrap/src/test/java/nd/jar/springhexboot/TestContainersConfiguration.java @@ -0,0 +1,35 @@ +package nd.jar.springhexboot; + +import com.redis.testcontainers.RedisContainer; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; +import org.springframework.context.annotation.Bean; +import org.testcontainers.containers.KafkaContainer; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.utility.DockerImageName; +import org.springframework.boot.devtools.restart.RestartScope; + +@TestConfiguration +public class TestContainersConfiguration { + @Bean + @RestartScope + @ServiceConnection + MySQLContainer mySQLContainer() { + return new MySQLContainer<>("mysql:5.7.39"); + } + + @Bean + @RestartScope + @ServiceConnection(name = "redis") + RedisContainer redisContainer(){ + return new RedisContainer(DockerImageName.parse("redis:6.2.6")); + } + + @Bean + @RestartScope + @ServiceConnection + KafkaContainer kafka() { + return new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1")); + } + +} diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 4955faf..0000000 --- a/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -plugins { - id 'org.springframework.boot' version '3.1.0' - id 'io.spring.dependency-management' version '1.0.11.RELEASE' -} - - -group = 'io.reflectoring.buckpal' -version = '0.0.1-SNAPSHOT' - -apply plugin: 'java' -apply plugin: 'io.spring.dependency-management' -apply plugin: 'java-library' - -repositories { - mavenCentral() -} - -compileJava { - sourceCompatibility = 17 - targetCompatibility = 17 -} - -dependencies { - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - - implementation ('org.springframework.boot:spring-boot-starter-web') - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - - testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.9.3' - testImplementation 'org.mockito:mockito-junit-jupiter:5.3.1' - testImplementation 'com.tngtech.archunit:archunit:1.0.1' - testImplementation 'com.h2database:h2' - - runtimeOnly 'com.h2database:h2' - -} - -test { - useJUnitPlatform() -} - diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle new file mode 100644 index 0000000..1957c33 --- /dev/null +++ b/buildSrc/build.gradle @@ -0,0 +1,3 @@ +plugins { + id 'groovy-gradle-plugin' +} \ No newline at end of file diff --git a/buildSrc/gradle/wrapper/gradle-wrapper.jar b/buildSrc/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..d64cd49 Binary files /dev/null and b/buildSrc/gradle/wrapper/gradle-wrapper.jar differ diff --git a/buildSrc/gradle/wrapper/gradle-wrapper.properties b/buildSrc/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1af9e09 --- /dev/null +++ b/buildSrc/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/buildSrc/gradlew b/buildSrc/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/buildSrc/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/buildSrc/gradlew.bat b/buildSrc/gradlew.bat new file mode 100644 index 0000000..93e3f59 --- /dev/null +++ b/buildSrc/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/buildSrc/src/main/groovy/adapter.java-conventions.gradle b/buildSrc/src/main/groovy/adapter.java-conventions.gradle new file mode 100644 index 0000000..36cc8ff --- /dev/null +++ b/buildSrc/src/main/groovy/adapter.java-conventions.gradle @@ -0,0 +1,12 @@ +plugins { + id 'core.java-conventions' +} + +dependencies { + implementation project(":application") + + implementation libs.jackson + implementation libs.mapstruct.core + annotationProcessor libs.mapstruct.processor + +} \ No newline at end of file diff --git a/buildSrc/src/main/groovy/core.java-conventions.gradle b/buildSrc/src/main/groovy/core.java-conventions.gradle new file mode 100644 index 0000000..9da798f --- /dev/null +++ b/buildSrc/src/main/groovy/core.java-conventions.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java-library' +} + +group = 'nd.jar.springhexboot' +version = '1.0.0' + +repositories { + mavenCentral() +} + +dependencies { + + implementation platform(libs.spring.boot.dependencies) + annotationProcessor enforcedPlatform(libs.spring.boot.dependencies) + testAnnotationProcessor enforcedPlatform(libs.spring.boot.dependencies) + + implementation libs.spring.context + compileOnly libs.lombok + annotationProcessor libs.lombok + + + implementation libs.log4j + + testImplementation platform('org.junit:junit-bom:5.9.1') + testImplementation libs.junit + testCompileOnly libs.lombok + testAnnotationProcessor libs.lombok +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..41cd7f9 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,29 @@ +[versions] +spring-boot = "3.2.5" +mapstruct = '1.5.5.Final' +mysql = '8.0.33' +openapi = '2.1.0' + +[libraries] +spring-boot-dependencies = { module = "org.springframework.boot:spring-boot-dependencies", version.ref = "spring-boot" } +spring-context = { module = "org.springframework:spring-context" } +spring-boot-starter-redis = { module = "org.springframework.boot:spring-boot-starter-data-redis" } +spring-boot-starter-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa" } +spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web"} +spring-kafka = {module = 'org.springframework.kafka:spring-kafka'} + +lombok = { module = "org.projectlombok:lombok" } +log4j = { module = 'org.apache.logging.log4j:log4j-api'} + +junit = { module = 'org.junit.jupiter:junit-jupiter'} + +jackson = { module = "com.fasterxml.jackson.core:jackson-databind" } +mapstruct-core = { module = 'org.mapstruct:mapstruct', version.ref = "mapstruct"} +mapstruct-processor = { module = 'org.mapstruct:mapstruct-processor', version.ref = "mapstruct"} + +apache-kafka = { module = 'org.apache.kafka:kafka-clients'} +connector-mysql = { module = 'mysql:mysql-connector-java', version.ref="mysql"} +swagger = { module = 'org.springdoc:springdoc-openapi-starter-webmvc-ui', version.ref='openapi' } + +[plugins] +#adapter = { id = 'adapter.java-conventions'} diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8049c68..48c0a02 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/img/cover-430.png b/img/cover-430.png deleted file mode 100644 index ed32cdb..0000000 Binary files a/img/cover-430.png and /dev/null differ diff --git a/img/cover-packt-450.png b/img/cover-packt-450.png deleted file mode 100644 index 71c887d..0000000 Binary files a/img/cover-packt-450.png and /dev/null differ diff --git a/img/dependencies.png b/img/dependencies.png deleted file mode 100644 index 571ecc8..0000000 Binary files a/img/dependencies.png and /dev/null differ diff --git a/img/hexagonal-architecture.png b/img/hexagonal-architecture.png index 316f6ad..f200228 100644 Binary files a/img/hexagonal-architecture.png and b/img/hexagonal-architecture.png differ diff --git a/in.http/build.gradle b/in.http/build.gradle new file mode 100644 index 0000000..b33ddc1 --- /dev/null +++ b/in.http/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'adapter.java-conventions' +} + +dependencies { + implementation libs.spring.boot.starter.web + implementation libs.swagger +} diff --git a/in.http/src/main/java/module-info.java b/in.http/src/main/java/module-info.java new file mode 100644 index 0000000..c6ca6e7 --- /dev/null +++ b/in.http/src/main/java/module-info.java @@ -0,0 +1,9 @@ +module spring.boot.hexagonal.in.http { + requires spring.boot.hexagonal.application; + requires spring.context; + requires spring.web; + requires static lombok; + requires org.mapstruct; + + opens nd.jar.springhexboot.adapter.in.http to spring.core, spring.beans; +} \ No newline at end of file diff --git a/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/Configuration.java b/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/Configuration.java new file mode 100644 index 0000000..7eea79d --- /dev/null +++ b/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/Configuration.java @@ -0,0 +1,8 @@ +package nd.jar.springhexboot.adapter.in.http; + +import org.springframework.context.annotation.ComponentScan; + +@org.springframework.context.annotation.Configuration +@ComponentScan +public class Configuration { +} diff --git a/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/EventDto.java b/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/EventDto.java new file mode 100644 index 0000000..0d1180c --- /dev/null +++ b/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/EventDto.java @@ -0,0 +1,8 @@ +package nd.jar.springhexboot.adapter.in.http; + +public record EventDto( + String id, + String name, + String description, + String from +){} \ No newline at end of file diff --git a/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/EventDtoMapper.java b/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/EventDtoMapper.java new file mode 100644 index 0000000..e1a3f7a --- /dev/null +++ b/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/EventDtoMapper.java @@ -0,0 +1,10 @@ +package nd.jar.springhexboot.adapter.in.http; + +import nd.jar.springhexboot.application.domain.model.Event; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface EventDtoMapper { + EventDto toDto(Event domain); + Event toDomainModel(EventDto dto); +} diff --git a/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/EventsController.java b/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/EventsController.java new file mode 100644 index 0000000..10b428f --- /dev/null +++ b/in.http/src/main/java/nd/jar/springhexboot/adapter/in/http/EventsController.java @@ -0,0 +1,38 @@ +package nd.jar.springhexboot.adapter.in.http; + +import lombok.RequiredArgsConstructor; +import nd.jar.springhexboot.application.port.in.FindEventsUseCase; +import nd.jar.springhexboot.application.port.in.PushEventUseCase; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +import static java.util.stream.Collectors.toMap; + +@RestController("/events") +@RequiredArgsConstructor +public class EventsController { + private final EventDtoMapper eventDtoMapper; + private final FindEventsUseCase findEventsUseCase; + private final PushEventUseCase pushEventUseCase; + + @PostMapping + ResponseEntity push(EventDto eventDto) { + pushEventUseCase.push(eventDtoMapper.toDomainModel(eventDto)); + return ResponseEntity.ok().build(); + } + + @GetMapping("/{id}") + Map get(@PathVariable("id") String id) { + return findEventsUseCase.find(id).entrySet() + .stream().collect(toMap(Map.Entry::getKey, entry -> eventDtoMapper.toDto(entry.getValue()))); + } + @GetMapping("/hello") + String hello() { + return "ok"; + } +} diff --git a/out.kafka/build.gradle b/out.kafka/build.gradle new file mode 100644 index 0000000..f7f1a13 --- /dev/null +++ b/out.kafka/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'adapter.java-conventions' +} + +dependencies { + implementation libs.spring.kafka + implementation libs.apache.kafka +} diff --git a/out.kafka/src/main/java/module-info.java b/out.kafka/src/main/java/module-info.java new file mode 100644 index 0000000..b392bf9 --- /dev/null +++ b/out.kafka/src/main/java/module-info.java @@ -0,0 +1,11 @@ +module spring.boot.hexagonal.out.kafka { + requires static lombok; + requires spring.context; + requires spring.boot.hexagonal.application; + requires spring.kafka; + requires com.fasterxml.jackson.databind; + requires org.apache.logging.log4j; + + + opens nd.jar.springhexboot.adapter.out.kafka to spring.core, spring.beans; +} \ No newline at end of file diff --git a/out.kafka/src/main/java/nd/jar/springhexboot/adapter/out/kafka/KafkaAdapter.java b/out.kafka/src/main/java/nd/jar/springhexboot/adapter/out/kafka/KafkaAdapter.java new file mode 100644 index 0000000..e688c45 --- /dev/null +++ b/out.kafka/src/main/java/nd/jar/springhexboot/adapter/out/kafka/KafkaAdapter.java @@ -0,0 +1,39 @@ +package nd.jar.springhexboot.adapter.out.kafka; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import nd.jar.springhexboot.application.domain.model.Event; +import nd.jar.springhexboot.application.port.out.ExternalStorage; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Log4j2 +@Service +@RequiredArgsConstructor +public class KafkaAdapter implements ExternalStorage { + private final KafkaTemplate template; + private final ObjectMapper om; + + @Override + public boolean push(Event event) { + log.info("KAFKA: Pushing event with id=`{}`", event.id()); + try { + template.send("events", event.id(), om.writeValueAsString(event)); + } catch (JsonProcessingException e) { + log.error("Error while processing json", e); + return false; + } + log.info("KAFKA: Pushed event with id=`{}`", event.id()); + return true; + } + + @Override + public Optional find(String id) { + log.error("Getting messages by id in kafka is not implemented"); + return Optional.empty(); + } +} diff --git a/out.persistence/build.gradle b/out.persistence/build.gradle new file mode 100644 index 0000000..60f45b1 --- /dev/null +++ b/out.persistence/build.gradle @@ -0,0 +1,8 @@ +plugins { + id 'adapter.java-conventions' +} + +dependencies { + implementation libs.spring.boot.starter.jpa + implementation libs.connector.mysql +} \ No newline at end of file diff --git a/out.persistence/src/main/java/module-info.java b/out.persistence/src/main/java/module-info.java new file mode 100644 index 0000000..83cba19 --- /dev/null +++ b/out.persistence/src/main/java/module-info.java @@ -0,0 +1,10 @@ +module spring.boot.hexagonal.out.persistence { + requires spring.boot.hexagonal.application; + requires jakarta.persistence; + requires static lombok; + requires org.mapstruct; + requires spring.data.jpa; + requires spring.context; + + opens nd.jar.springhexboot.adapter.out.persistence to spring.core; +} \ No newline at end of file diff --git a/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventEntity.java b/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventEntity.java new file mode 100644 index 0000000..639b3f4 --- /dev/null +++ b/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventEntity.java @@ -0,0 +1,24 @@ +package nd.jar.springhexboot.adapter.out.persistence; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table +@Getter +@Setter +@ToString +@RequiredArgsConstructor +public class EventEntity { + @Id + private String id; + + private String name; + + private String description; + + @Column(name = "`from`") + private String from; + + +} diff --git a/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventEntityMapper.java b/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventEntityMapper.java new file mode 100644 index 0000000..024df38 --- /dev/null +++ b/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventEntityMapper.java @@ -0,0 +1,11 @@ +package nd.jar.springhexboot.adapter.out.persistence; + +import nd.jar.springhexboot.application.domain.model.Event; +import org.mapstruct.Mapper; + +@Mapper(componentModel = "spring") +public interface EventEntityMapper { + Event toDomainModel(EventEntity entity); + EventEntity toEntity(Event domainModel); + +} diff --git a/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventPersistenceAdapter.java b/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventPersistenceAdapter.java new file mode 100644 index 0000000..9cccd9d --- /dev/null +++ b/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventPersistenceAdapter.java @@ -0,0 +1,28 @@ +package nd.jar.springhexboot.adapter.out.persistence; + +import lombok.RequiredArgsConstructor; +import nd.jar.springhexboot.application.domain.model.Event; +import nd.jar.springhexboot.application.port.out.ExternalStorage; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@EnableJpaRepositories(basePackageClasses = EventRepository.class) +@RequiredArgsConstructor +@Service +public class EventPersistenceAdapter implements ExternalStorage { + private final EventRepository eventRepository; + private final EventEntityMapper accountMapper; + + @Override + public boolean push(Event event) { + eventRepository.save(accountMapper.toEntity(event)); + return true; + } + + @Override + public Optional find(String id) { + return eventRepository.findById(id).map(accountMapper::toDomainModel); + } +} diff --git a/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventRepository.java b/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventRepository.java new file mode 100644 index 0000000..437f259 --- /dev/null +++ b/out.persistence/src/main/java/nd/jar/springhexboot/adapter/out/persistence/EventRepository.java @@ -0,0 +1,5 @@ +package nd.jar.springhexboot.adapter.out.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EventRepository extends JpaRepository {} diff --git a/out.redis/build.gradle b/out.redis/build.gradle new file mode 100644 index 0000000..ef88d4b --- /dev/null +++ b/out.redis/build.gradle @@ -0,0 +1,7 @@ +plugins { + id 'adapter.java-conventions' +} + +dependencies { + implementation libs.spring.boot.starter.redis +} \ No newline at end of file diff --git a/out.redis/src/main/java/module-info.java b/out.redis/src/main/java/module-info.java new file mode 100644 index 0000000..2973c22 --- /dev/null +++ b/out.redis/src/main/java/module-info.java @@ -0,0 +1,11 @@ +module spring.boot.hexagonal.out.redis { + requires static lombok; + requires spring.context; + requires spring.data.redis; + requires com.fasterxml.jackson.databind; + requires spring.boot.hexagonal.application; + requires org.apache.logging.log4j; + + opens nd.jar.springhexboot.adapter.out.redis to spring.core, spring.beans; + +} \ No newline at end of file diff --git a/out.redis/src/main/java/nd/jar/springhexboot/adapter/out/redis/RedisCacheAdapter.java b/out.redis/src/main/java/nd/jar/springhexboot/adapter/out/redis/RedisCacheAdapter.java new file mode 100644 index 0000000..e018604 --- /dev/null +++ b/out.redis/src/main/java/nd/jar/springhexboot/adapter/out/redis/RedisCacheAdapter.java @@ -0,0 +1,43 @@ +package nd.jar.springhexboot.adapter.out.redis; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; +import nd.jar.springhexboot.application.domain.model.Event; +import nd.jar.springhexboot.application.port.out.ExternalStorage; +import nd.jar.springhexboot.application.port.out.GetEventPort; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +@Service +@Log4j2 +@RequiredArgsConstructor +public class RedisCacheAdapter implements ExternalStorage, GetEventPort { + private final StringRedisTemplate stringRedisTemplate; + private final ObjectMapper om; + @Override + public boolean push(Event event) { + try { + stringRedisTemplate.opsForValue().set(event.id(), om.writeValueAsString(event)); + } catch (JsonProcessingException e) { + log.error("Error while processing json", e); + return false; + } + return true; + } + + @Override + public Optional find(String id) { + final var stringResult = stringRedisTemplate.opsForValue().get(id); + try { + final var result = om.readValue(stringResult, Event.class); + return Optional.of(result); + } catch (JsonProcessingException e) { + log.error("Error while processing json", e); + return Optional.empty(); + } + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..c5e0a2b --- /dev/null +++ b/settings.gradle @@ -0,0 +1,8 @@ +rootProject.name = 'spring-boot-hexagonal' +include 'in.http' +include 'out.redis' +include 'out.kafka' +include 'out.persistence' +include 'application' +include 'bootstrap' + diff --git a/src/main/java/io/reflectoring/buckpal/BuckPalApplication.java b/src/main/java/io/reflectoring/buckpal/BuckPalApplication.java deleted file mode 100644 index 3cd06ed..0000000 --- a/src/main/java/io/reflectoring/buckpal/BuckPalApplication.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.reflectoring.buckpal; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; - -@SpringBootApplication -public class BuckPalApplication { - - public static void main(String[] args) { - SpringApplication.run(BuckPalApplication.class, args); - } - -} diff --git a/src/main/java/io/reflectoring/buckpal/BuckPalConfiguration.java b/src/main/java/io/reflectoring/buckpal/BuckPalConfiguration.java deleted file mode 100644 index a74f05d..0000000 --- a/src/main/java/io/reflectoring/buckpal/BuckPalConfiguration.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.reflectoring.buckpal; - -import io.reflectoring.buckpal.application.domain.service.MoneyTransferProperties; -import io.reflectoring.buckpal.application.domain.model.Money; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@EnableConfigurationProperties(BuckPalConfigurationProperties.class) -public class BuckPalConfiguration { - - /** - * Adds a use-case-specific {@link MoneyTransferProperties} object to the application context. The properties - * are read from the Spring-Boot-specific {@link BuckPalConfigurationProperties} object. - */ - @Bean - public MoneyTransferProperties moneyTransferProperties(BuckPalConfigurationProperties buckPalConfigurationProperties){ - return new MoneyTransferProperties(Money.of(buckPalConfigurationProperties.getTransferThreshold())); - } - -} diff --git a/src/main/java/io/reflectoring/buckpal/BuckPalConfigurationProperties.java b/src/main/java/io/reflectoring/buckpal/BuckPalConfigurationProperties.java deleted file mode 100644 index 6da5b42..0000000 --- a/src/main/java/io/reflectoring/buckpal/BuckPalConfigurationProperties.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.reflectoring.buckpal; - -import lombok.Data; -import org.springframework.boot.context.properties.ConfigurationProperties; - -@Data -@ConfigurationProperties(prefix = "buckpal") -public class BuckPalConfigurationProperties { - - private long transferThreshold = Long.MAX_VALUE; - -} diff --git a/src/main/java/io/reflectoring/buckpal/adapter/in/web/SendMoneyController.java b/src/main/java/io/reflectoring/buckpal/adapter/in/web/SendMoneyController.java deleted file mode 100644 index 19a1b90..0000000 --- a/src/main/java/io/reflectoring/buckpal/adapter/in/web/SendMoneyController.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.reflectoring.buckpal.adapter.in.web; - -import io.reflectoring.buckpal.application.port.in.SendMoneyUseCase; -import io.reflectoring.buckpal.application.port.in.SendMoneyCommand; -import io.reflectoring.buckpal.common.WebAdapter; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.Money; -import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RestController; - -@WebAdapter -@RestController -@RequiredArgsConstructor -class SendMoneyController { - - private final SendMoneyUseCase sendMoneyUseCase; - - @PostMapping(path = "/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}") - void sendMoney( - @PathVariable("sourceAccountId") Long sourceAccountId, - @PathVariable("targetAccountId") Long targetAccountId, - @PathVariable("amount") Long amount) { - - SendMoneyCommand command = new SendMoneyCommand( - new AccountId(sourceAccountId), - new AccountId(targetAccountId), - Money.of(amount)); - - sendMoneyUseCase.sendMoney(command); - } - -} diff --git a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/AccountJpaEntity.java b/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/AccountJpaEntity.java deleted file mode 100644 index 7dfab52..0000000 --- a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/AccountJpaEntity.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.reflectoring.buckpal.adapter.out.persistence; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "account") -@Data -@AllArgsConstructor -@NoArgsConstructor -class AccountJpaEntity { - - @Id - @GeneratedValue - private Long id; - -} diff --git a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/AccountMapper.java b/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/AccountMapper.java deleted file mode 100644 index 5d2ebf5..0000000 --- a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/AccountMapper.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.reflectoring.buckpal.adapter.out.persistence; - -import java.util.ArrayList; -import java.util.List; - -import io.reflectoring.buckpal.application.domain.model.Account; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.Activity; -import io.reflectoring.buckpal.application.domain.model.Activity.ActivityId; -import io.reflectoring.buckpal.application.domain.model.ActivityWindow; -import io.reflectoring.buckpal.application.domain.model.Money; -import org.springframework.stereotype.Component; - -@Component -class AccountMapper { - - Account mapToDomainEntity( - AccountJpaEntity account, - List activities, - Long withdrawalBalance, - Long depositBalance) { - - Money baselineBalance = Money.subtract( - Money.of(depositBalance), - Money.of(withdrawalBalance)); - - return Account.withId( - new AccountId(account.getId()), - baselineBalance, - mapToActivityWindow(activities)); - - } - - ActivityWindow mapToActivityWindow(List activities) { - List mappedActivities = new ArrayList<>(); - - for (ActivityJpaEntity activity : activities) { - mappedActivities.add(new Activity( - new ActivityId(activity.getId()), - new AccountId(activity.getOwnerAccountId()), - new AccountId(activity.getSourceAccountId()), - new AccountId(activity.getTargetAccountId()), - activity.getTimestamp(), - Money.of(activity.getAmount()))); - } - - return new ActivityWindow(mappedActivities); - } - - ActivityJpaEntity mapToJpaEntity(Activity activity) { - return new ActivityJpaEntity( - activity.getId() == null ? null : activity.getId().getValue(), - activity.getTimestamp(), - activity.getOwnerAccountId().getValue(), - activity.getSourceAccountId().getValue(), - activity.getTargetAccountId().getValue(), - activity.getMoney().getAmount().longValue()); - } - -} diff --git a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/AccountPersistenceAdapter.java b/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/AccountPersistenceAdapter.java deleted file mode 100644 index 26b427b..0000000 --- a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/AccountPersistenceAdapter.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.reflectoring.buckpal.adapter.out.persistence; - -import io.reflectoring.buckpal.application.domain.model.Account; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.Activity; -import io.reflectoring.buckpal.application.port.out.LoadAccountPort; -import io.reflectoring.buckpal.application.port.out.UpdateAccountStatePort; -import io.reflectoring.buckpal.common.PersistenceAdapter; -import lombok.RequiredArgsConstructor; - -import jakarta.persistence.EntityNotFoundException; -import java.time.LocalDateTime; -import java.util.List; - -@RequiredArgsConstructor -@PersistenceAdapter -class AccountPersistenceAdapter implements - LoadAccountPort, - UpdateAccountStatePort { - - private final SpringDataAccountRepository accountRepository; - private final ActivityRepository activityRepository; - private final AccountMapper accountMapper; - - @Override - public Account loadAccount( - AccountId accountId, - LocalDateTime baselineDate) { - - AccountJpaEntity account = - accountRepository.findById(accountId.getValue()) - .orElseThrow(EntityNotFoundException::new); - - List activities = - activityRepository.findByOwnerSince( - accountId.getValue(), - baselineDate); - - Long withdrawalBalance = activityRepository - .getWithdrawalBalanceUntil( - accountId.getValue(), - baselineDate) - .orElse(0L); - - Long depositBalance = activityRepository - .getDepositBalanceUntil( - accountId.getValue(), - baselineDate) - .orElse(0L); - - return accountMapper.mapToDomainEntity( - account, - activities, - withdrawalBalance, - depositBalance); - - } - - @Override - public void updateActivities(Account account) { - for (Activity activity : account.getActivityWindow().getActivities()) { - if (activity.getId() == null) { - activityRepository.save(accountMapper.mapToJpaEntity(activity)); - } - } - } - -} diff --git a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/ActivityJpaEntity.java b/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/ActivityJpaEntity.java deleted file mode 100644 index 89c0551..0000000 --- a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/ActivityJpaEntity.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.reflectoring.buckpal.adapter.out.persistence; - -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - -import java.time.LocalDateTime; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Entity -@Table(name = "activity") -@Data -@AllArgsConstructor -@NoArgsConstructor -class ActivityJpaEntity { - - @Id - @GeneratedValue - private Long id; - - @Column - private LocalDateTime timestamp; - - @Column - private long ownerAccountId; - - @Column - private long sourceAccountId; - - @Column - private long targetAccountId; - - @Column - private long amount; - -} diff --git a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/ActivityRepository.java b/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/ActivityRepository.java deleted file mode 100644 index 82abaad..0000000 --- a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/ActivityRepository.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.reflectoring.buckpal.adapter.out.persistence; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; - -interface ActivityRepository extends JpaRepository { - - @Query(""" - select a from ActivityJpaEntity a - where a.ownerAccountId = :ownerAccountId - and a.timestamp >= :since - """) - List findByOwnerSince( - @Param("ownerAccountId") long ownerAccountId, - @Param("since") LocalDateTime since); - - @Query(""" - select sum(a.amount) from ActivityJpaEntity a - where a.targetAccountId = :accountId - and a.ownerAccountId = :accountId - and a.timestamp < :until - """) - Optional getDepositBalanceUntil( - @Param("accountId") long accountId, - @Param("until") LocalDateTime until); - - @Query(""" - select sum(a.amount) from ActivityJpaEntity a - where a.sourceAccountId = :accountId - and a.ownerAccountId = :accountId - and a.timestamp < :until - """) - Optional getWithdrawalBalanceUntil( - @Param("accountId") long accountId, - @Param("until") LocalDateTime until); - -} diff --git a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/NoOpAccountLock.java b/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/NoOpAccountLock.java deleted file mode 100644 index 136f816..0000000 --- a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/NoOpAccountLock.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.reflectoring.buckpal.adapter.out.persistence; - -import io.reflectoring.buckpal.application.port.out.AccountLock; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import org.springframework.stereotype.Component; - -@Component -class NoOpAccountLock implements AccountLock { - - @Override - public void lockAccount(AccountId accountId) { - // do nothing - } - - @Override - public void releaseAccount(AccountId accountId) { - // do nothing - } - -} diff --git a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/SpringDataAccountRepository.java b/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/SpringDataAccountRepository.java deleted file mode 100644 index ae15293..0000000 --- a/src/main/java/io/reflectoring/buckpal/adapter/out/persistence/SpringDataAccountRepository.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.reflectoring.buckpal.adapter.out.persistence; - -import org.springframework.data.jpa.repository.JpaRepository; - -interface SpringDataAccountRepository extends JpaRepository { -} diff --git a/src/main/java/io/reflectoring/buckpal/application/domain/model/Account.java b/src/main/java/io/reflectoring/buckpal/application/domain/model/Account.java deleted file mode 100644 index 7044ac4..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/domain/model/Account.java +++ /dev/null @@ -1,118 +0,0 @@ -package io.reflectoring.buckpal.application.domain.model; - -import java.time.LocalDateTime; -import java.util.Optional; - -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Value; - -/** - * An account that holds a certain amount of money. An {@link Account} object only - * contains a window of the latest account activities. The total balance of the account is - * the sum of a baseline balance that was valid before the first activity in the - * window and the sum of the activity values. - */ -@AllArgsConstructor(access = AccessLevel.PRIVATE) -public class Account { - - /** - * The unique ID of the account. - */ - private final AccountId id; - - /** - * The baseline balance of the account. This was the balance of the account before the first - * activity in the activityWindow. - */ - @Getter private final Money baselineBalance; - - /** - * The window of latest activities on this account. - */ - @Getter private final ActivityWindow activityWindow; - - /** - * Creates an {@link Account} entity without an ID. Use to create a new entity that is not yet - * persisted. - */ - public static Account withoutId( - Money baselineBalance, - ActivityWindow activityWindow) { - return new Account(null, baselineBalance, activityWindow); - } - - /** - * Creates an {@link Account} entity with an ID. Use to reconstitute a persisted entity. - */ - public static Account withId( - AccountId accountId, - Money baselineBalance, - ActivityWindow activityWindow) { - return new Account(accountId, baselineBalance, activityWindow); - } - - public Optional getId(){ - return Optional.ofNullable(this.id); - } - - /** - * Calculates the total balance of the account by adding the activity values to the baseline balance. - */ - public Money calculateBalance() { - return Money.add( - this.baselineBalance, - this.activityWindow.calculateBalance(this.id)); - } - - /** - * Tries to withdraw a certain amount of money from this account. - * If successful, creates a new activity with a negative value. - * @return true if the withdrawal was successful, false if not. - */ - public boolean withdraw(Money money, AccountId targetAccountId) { - - if (!mayWithdraw(money)) { - return false; - } - - Activity withdrawal = new Activity( - this.id, - this.id, - targetAccountId, - LocalDateTime.now(), - money); - this.activityWindow.addActivity(withdrawal); - return true; - } - - private boolean mayWithdraw(Money money) { - return Money.add( - this.calculateBalance(), - money.negate()) - .isPositiveOrZero(); - } - - /** - * Tries to deposit a certain amount of money to this account. - * If sucessful, creates a new activity with a positive value. - * @return true if the deposit was successful, false if not. - */ - public boolean deposit(Money money, AccountId sourceAccountId) { - Activity deposit = new Activity( - this.id, - sourceAccountId, - this.id, - LocalDateTime.now(), - money); - this.activityWindow.addActivity(deposit); - return true; - } - - @Value - public static class AccountId { - private Long value; - } - -} diff --git a/src/main/java/io/reflectoring/buckpal/application/domain/model/Activity.java b/src/main/java/io/reflectoring/buckpal/application/domain/model/Activity.java deleted file mode 100644 index 8c70e9f..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/domain/model/Activity.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.reflectoring.buckpal.application.domain.model; - -import java.time.LocalDateTime; - -import lombok.Getter; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.Value; - -/** - * A money transfer activity between {@link Account}s. - */ -@Value -@RequiredArgsConstructor -public class Activity { - - @Getter - private ActivityId id; - - /** - * The account that owns this activity. - */ - @Getter - @NonNull - private final Account.AccountId ownerAccountId; - - /** - * The debited account. - */ - @Getter - @NonNull - private final Account.AccountId sourceAccountId; - - /** - * The credited account. - */ - @Getter - @NonNull - private final Account.AccountId targetAccountId; - - /** - * The timestamp of the activity. - */ - @Getter - @NonNull - private final LocalDateTime timestamp; - - /** - * The money that was transferred between the accounts. - */ - @Getter - @NonNull - private final Money money; - - public Activity( - @NonNull Account.AccountId ownerAccountId, - @NonNull Account.AccountId sourceAccountId, - @NonNull Account.AccountId targetAccountId, - @NonNull LocalDateTime timestamp, - @NonNull Money money) { - this.id = null; - this.ownerAccountId = ownerAccountId; - this.sourceAccountId = sourceAccountId; - this.targetAccountId = targetAccountId; - this.timestamp = timestamp; - this.money = money; - } - - @Value - public static class ActivityId { - private final Long value; - } - -} diff --git a/src/main/java/io/reflectoring/buckpal/application/domain/model/ActivityWindow.java b/src/main/java/io/reflectoring/buckpal/application/domain/model/ActivityWindow.java deleted file mode 100644 index 374b9cc..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/domain/model/ActivityWindow.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.reflectoring.buckpal.application.domain.model; - -import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; - -import lombok.NonNull; - -/** - * A window of account activities. - */ -public class ActivityWindow { - - /** - * The list of account activities within this window. - */ - private List activities; - - /** - * The timestamp of the first activity within this window. - */ - public LocalDateTime getStartTimestamp() { - return activities.stream() - .min(Comparator.comparing(Activity::getTimestamp)) - .orElseThrow(IllegalStateException::new) - .getTimestamp(); - } - - /** - * The timestamp of the last activity within this window. - * @return - */ - public LocalDateTime getEndTimestamp() { - return activities.stream() - .max(Comparator.comparing(Activity::getTimestamp)) - .orElseThrow(IllegalStateException::new) - .getTimestamp(); - } - - /** - * Calculates the balance by summing up the values of all activities within this window. - */ - public Money calculateBalance(Account.AccountId accountId) { - Money depositBalance = activities.stream() - .filter(a -> a.getTargetAccountId().equals(accountId)) - .map(Activity::getMoney) - .reduce(Money.ZERO, Money::add); - - Money withdrawalBalance = activities.stream() - .filter(a -> a.getSourceAccountId().equals(accountId)) - .map(Activity::getMoney) - .reduce(Money.ZERO, Money::add); - - return Money.add(depositBalance, withdrawalBalance.negate()); - } - - public ActivityWindow(@NonNull List activities) { - this.activities = activities; - } - - public ActivityWindow(@NonNull Activity... activities) { - this.activities = new ArrayList<>(Arrays.asList(activities)); - } - - public List getActivities() { - return Collections.unmodifiableList(this.activities); - } - - public void addActivity(Activity activity) { - this.activities.add(activity); - } -} diff --git a/src/main/java/io/reflectoring/buckpal/application/domain/model/Money.java b/src/main/java/io/reflectoring/buckpal/application/domain/model/Money.java deleted file mode 100644 index 736384a..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/domain/model/Money.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.reflectoring.buckpal.application.domain.model; - -import java.math.BigInteger; - -import lombok.NonNull; -import lombok.Value; - -@Value -public class Money { - - public static Money ZERO = Money.of(0L); - - @NonNull - private final BigInteger amount; - - public boolean isPositiveOrZero(){ - return this.amount.compareTo(BigInteger.ZERO) >= 0; - } - - public boolean isNegative(){ - return this.amount.compareTo(BigInteger.ZERO) < 0; - } - - public boolean isPositive(){ - return this.amount.compareTo(BigInteger.ZERO) > 0; - } - - public boolean isGreaterThanOrEqualTo(Money money){ - return this.amount.compareTo(money.amount) >= 0; - } - - public boolean isGreaterThan(Money money){ - return this.amount.compareTo(money.amount) >= 1; - } - - public static Money of(long value) { - return new Money(BigInteger.valueOf(value)); - } - - public static Money add(Money a, Money b) { - return new Money(a.amount.add(b.amount)); - } - - public Money minus(Money money){ - return new Money(this.amount.subtract(money.amount)); - } - - public Money plus(Money money){ - return new Money(this.amount.add(money.amount)); - } - - public static Money subtract(Money a, Money b) { - return new Money(a.amount.subtract(b.amount)); - } - - public Money negate(){ - return new Money(this.amount.negate()); - } - -} diff --git a/src/main/java/io/reflectoring/buckpal/application/domain/service/GetAccountBalanceService.java b/src/main/java/io/reflectoring/buckpal/application/domain/service/GetAccountBalanceService.java deleted file mode 100644 index 6a48ff5..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/domain/service/GetAccountBalanceService.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.reflectoring.buckpal.application.domain.service; - -import io.reflectoring.buckpal.application.domain.model.Money; -import io.reflectoring.buckpal.application.port.in.GetAccountBalanceUseCase; -import io.reflectoring.buckpal.application.port.out.LoadAccountPort; -import lombok.RequiredArgsConstructor; - -import java.time.LocalDateTime; - -@RequiredArgsConstructor -class GetAccountBalanceService implements GetAccountBalanceUseCase { - - private final LoadAccountPort loadAccountPort; - - @Override - public Money getAccountBalance(GetAccountBalanceQuery query) { - return loadAccountPort.loadAccount(query.accountId(), LocalDateTime.now()) - .calculateBalance(); - } -} diff --git a/src/main/java/io/reflectoring/buckpal/application/domain/service/MoneyTransferProperties.java b/src/main/java/io/reflectoring/buckpal/application/domain/service/MoneyTransferProperties.java deleted file mode 100644 index 2a4f7cd..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/domain/service/MoneyTransferProperties.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.reflectoring.buckpal.application.domain.service; - -import io.reflectoring.buckpal.application.domain.model.Money; -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * Configuration properties for money transfer use cases. - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -public class MoneyTransferProperties { - - private Money maximumTransferThreshold = Money.of(1_000_000L); - -} diff --git a/src/main/java/io/reflectoring/buckpal/application/domain/service/SendMoneyService.java b/src/main/java/io/reflectoring/buckpal/application/domain/service/SendMoneyService.java deleted file mode 100644 index a97f48f..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/domain/service/SendMoneyService.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.reflectoring.buckpal.application.domain.service; - -import io.reflectoring.buckpal.application.port.in.SendMoneyCommand; -import io.reflectoring.buckpal.application.port.in.SendMoneyUseCase; -import io.reflectoring.buckpal.application.port.out.AccountLock; -import io.reflectoring.buckpal.application.port.out.LoadAccountPort; -import io.reflectoring.buckpal.application.port.out.UpdateAccountStatePort; -import io.reflectoring.buckpal.common.UseCase; -import io.reflectoring.buckpal.application.domain.model.Account; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import lombok.RequiredArgsConstructor; - -import jakarta.transaction.Transactional; -import java.time.LocalDateTime; - -@RequiredArgsConstructor -@UseCase -@Transactional -public class SendMoneyService implements SendMoneyUseCase { - - private final LoadAccountPort loadAccountPort; - private final AccountLock accountLock; - private final UpdateAccountStatePort updateAccountStatePort; - private final MoneyTransferProperties moneyTransferProperties; - - @Override - public boolean sendMoney(SendMoneyCommand command) { - - checkThreshold(command); - - LocalDateTime baselineDate = LocalDateTime.now().minusDays(10); - - Account sourceAccount = loadAccountPort.loadAccount( - command.sourceAccountId(), - baselineDate); - - Account targetAccount = loadAccountPort.loadAccount( - command.targetAccountId(), - baselineDate); - - AccountId sourceAccountId = sourceAccount.getId() - .orElseThrow(() -> new IllegalStateException("expected source account ID not to be empty")); - AccountId targetAccountId = targetAccount.getId() - .orElseThrow(() -> new IllegalStateException("expected target account ID not to be empty")); - - accountLock.lockAccount(sourceAccountId); - if (!sourceAccount.withdraw(command.money(), targetAccountId)) { - accountLock.releaseAccount(sourceAccountId); - return false; - } - - accountLock.lockAccount(targetAccountId); - if (!targetAccount.deposit(command.money(), sourceAccountId)) { - accountLock.releaseAccount(sourceAccountId); - accountLock.releaseAccount(targetAccountId); - return false; - } - - updateAccountStatePort.updateActivities(sourceAccount); - updateAccountStatePort.updateActivities(targetAccount); - - accountLock.releaseAccount(sourceAccountId); - accountLock.releaseAccount(targetAccountId); - return true; - } - - private void checkThreshold(SendMoneyCommand command) { - if(command.money().isGreaterThan(moneyTransferProperties.getMaximumTransferThreshold())){ - throw new ThresholdExceededException(moneyTransferProperties.getMaximumTransferThreshold(), command.money()); - } - } - -} - - - - diff --git a/src/main/java/io/reflectoring/buckpal/application/domain/service/ThresholdExceededException.java b/src/main/java/io/reflectoring/buckpal/application/domain/service/ThresholdExceededException.java deleted file mode 100644 index 8c21a17..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/domain/service/ThresholdExceededException.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.reflectoring.buckpal.application.domain.service; - -import io.reflectoring.buckpal.application.domain.model.Money; - -public class ThresholdExceededException extends RuntimeException { - - public ThresholdExceededException(Money threshold, Money actual) { - super(String.format("Maximum threshold for transferring money exceeded: tried to transfer %s but threshold is %s!", actual, threshold)); - } - -} diff --git a/src/main/java/io/reflectoring/buckpal/application/port/in/GetAccountBalanceUseCase.java b/src/main/java/io/reflectoring/buckpal/application/port/in/GetAccountBalanceUseCase.java deleted file mode 100644 index d44f828..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/port/in/GetAccountBalanceUseCase.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.reflectoring.buckpal.application.port.in; - -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.Money; - -public interface GetAccountBalanceUseCase { - - Money getAccountBalance(GetAccountBalanceQuery query); - - record GetAccountBalanceQuery(AccountId accountId) { - } -} diff --git a/src/main/java/io/reflectoring/buckpal/application/port/in/PositiveMoney.java b/src/main/java/io/reflectoring/buckpal/application/port/in/PositiveMoney.java deleted file mode 100644 index aaf8136..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/port/in/PositiveMoney.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.reflectoring.buckpal.application.port.in; - -import io.reflectoring.buckpal.application.domain.model.Money; -import jakarta.validation.Constraint; - -import java.lang.annotation.Documented; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Target({ FIELD }) -@Retention(RUNTIME) -@Constraint(validatedBy = PositiveMoneyValidator.class) -@Documented -public @interface PositiveMoney { - - String message() default "must be positive" + - " found: ${validatedValue}"; - - Class[] groups() default {}; - - Class[] payload() default {}; - -} diff --git a/src/main/java/io/reflectoring/buckpal/application/port/in/PositiveMoneyValidator.java b/src/main/java/io/reflectoring/buckpal/application/port/in/PositiveMoneyValidator.java deleted file mode 100644 index 80378d6..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/port/in/PositiveMoneyValidator.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.reflectoring.buckpal.application.port.in; - -import io.reflectoring.buckpal.application.domain.model.Money; -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; - -public class PositiveMoneyValidator implements ConstraintValidator { - - @Override - public boolean isValid(Money value, ConstraintValidatorContext context) { - return value.isPositive(); - } -} diff --git a/src/main/java/io/reflectoring/buckpal/application/port/in/SendMoneyCommand.java b/src/main/java/io/reflectoring/buckpal/application/port/in/SendMoneyCommand.java deleted file mode 100644 index e9dcd12..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/port/in/SendMoneyCommand.java +++ /dev/null @@ -1,25 +0,0 @@ -package io.reflectoring.buckpal.application.port.in; - -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.Money; -import jakarta.validation.constraints.NotNull; - -import static io.reflectoring.buckpal.common.validation.Validation.validate; - -public record SendMoneyCommand( - @NotNull AccountId sourceAccountId, - @NotNull AccountId targetAccountId, - @NotNull @PositiveMoney Money money -) { - - public SendMoneyCommand( - AccountId sourceAccountId, - AccountId targetAccountId, - Money money) { - this.sourceAccountId = sourceAccountId; - this.targetAccountId = targetAccountId; - this.money = money; - validate(this); - } - -} diff --git a/src/main/java/io/reflectoring/buckpal/application/port/in/SendMoneyUseCase.java b/src/main/java/io/reflectoring/buckpal/application/port/in/SendMoneyUseCase.java deleted file mode 100644 index 3d86aee..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/port/in/SendMoneyUseCase.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.reflectoring.buckpal.application.port.in; - -public interface SendMoneyUseCase { - - boolean sendMoney(SendMoneyCommand command); - -} diff --git a/src/main/java/io/reflectoring/buckpal/application/port/out/AccountLock.java b/src/main/java/io/reflectoring/buckpal/application/port/out/AccountLock.java deleted file mode 100644 index fa1772a..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/port/out/AccountLock.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.reflectoring.buckpal.application.port.out; - -import io.reflectoring.buckpal.application.domain.model.Account; - -public interface AccountLock { - - void lockAccount(Account.AccountId accountId); - - void releaseAccount(Account.AccountId accountId); - -} diff --git a/src/main/java/io/reflectoring/buckpal/application/port/out/LoadAccountPort.java b/src/main/java/io/reflectoring/buckpal/application/port/out/LoadAccountPort.java deleted file mode 100644 index 3d1acc6..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/port/out/LoadAccountPort.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.reflectoring.buckpal.application.port.out; - -import java.time.LocalDateTime; - -import io.reflectoring.buckpal.application.domain.model.Account; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; - -public interface LoadAccountPort { - - Account loadAccount(AccountId accountId, LocalDateTime baselineDate); -} diff --git a/src/main/java/io/reflectoring/buckpal/application/port/out/UpdateAccountStatePort.java b/src/main/java/io/reflectoring/buckpal/application/port/out/UpdateAccountStatePort.java deleted file mode 100644 index ce22b2e..0000000 --- a/src/main/java/io/reflectoring/buckpal/application/port/out/UpdateAccountStatePort.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.reflectoring.buckpal.application.port.out; - -import io.reflectoring.buckpal.application.domain.model.Account; - -public interface UpdateAccountStatePort { - - void updateActivities(Account account); - -} diff --git a/src/main/java/io/reflectoring/buckpal/common/PersistenceAdapter.java b/src/main/java/io/reflectoring/buckpal/common/PersistenceAdapter.java deleted file mode 100644 index d76fdec..0000000 --- a/src/main/java/io/reflectoring/buckpal/common/PersistenceAdapter.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.reflectoring.buckpal.common; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.core.annotation.AliasFor; -import org.springframework.stereotype.Component; - -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Component -public @interface PersistenceAdapter { - - /** - * The value may indicate a suggestion for a logical component name, - * to be turned into a Spring bean in case of an autodetected component. - * @return the suggested component name, if any (or empty String otherwise) - */ - @AliasFor(annotation = Component.class) - String value() default ""; - -} diff --git a/src/main/java/io/reflectoring/buckpal/common/UseCase.java b/src/main/java/io/reflectoring/buckpal/common/UseCase.java deleted file mode 100644 index eca4e05..0000000 --- a/src/main/java/io/reflectoring/buckpal/common/UseCase.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.reflectoring.buckpal.common; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -import org.springframework.core.annotation.AliasFor; -import org.springframework.stereotype.Component; - -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Component -public @interface UseCase { - - /** - * The value may indicate a suggestion for a logical component name, - * to be turned into a Spring bean in case of an autodetected component. - * @return the suggested component name, if any (or empty String otherwise) - */ - @AliasFor(annotation = Component.class) - String value() default ""; - -} diff --git a/src/main/java/io/reflectoring/buckpal/common/WebAdapter.java b/src/main/java/io/reflectoring/buckpal/common/WebAdapter.java deleted file mode 100644 index db25ec4..0000000 --- a/src/main/java/io/reflectoring/buckpal/common/WebAdapter.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.reflectoring.buckpal.common; - -import org.springframework.core.annotation.AliasFor; -import org.springframework.stereotype.Component; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Documented -@Component -public @interface WebAdapter { - - /** - * The value may indicate a suggestion for a logical component name, - * to be turned into a Spring bean in case of an autodetected component. - * @return the suggested component name, if any (or empty String otherwise) - */ - @AliasFor(annotation = Component.class) - String value() default ""; - -} diff --git a/src/main/java/io/reflectoring/buckpal/common/validation/Validation.java b/src/main/java/io/reflectoring/buckpal/common/validation/Validation.java deleted file mode 100644 index 6831775..0000000 --- a/src/main/java/io/reflectoring/buckpal/common/validation/Validation.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.reflectoring.buckpal.common.validation; - -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; -import jakarta.validation.Validator; -import java.util.Set; - -import static jakarta.validation.Validation.buildDefaultValidatorFactory; - -public class Validation { - - // Your IDE may complain that the ValidatorFactory needs to be closed, but if we do that here, - // we break the contract of ValidatorFactory#close. - private final static Validator validator = - buildDefaultValidatorFactory().getValidator(); - - /** - * Evaluates all Bean Validation annotations on the subject. - */ - public static void validate(T subject) { - Set> violations = validator.validate(subject); - if (!violations.isEmpty()) { - throw new ConstraintViolationException(violations); - } - } -} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml deleted file mode 100644 index bb4bc27..0000000 --- a/src/main/resources/application.yml +++ /dev/null @@ -1,2 +0,0 @@ -buckpal: - transferThreshold: 10000 \ No newline at end of file diff --git a/src/test/java/io/reflectoring/buckpal/BuckPalApplicationTests.java b/src/test/java/io/reflectoring/buckpal/BuckPalApplicationTests.java deleted file mode 100644 index 9943fe0..0000000 --- a/src/test/java/io/reflectoring/buckpal/BuckPalApplicationTests.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.reflectoring.buckpal; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.junit.jupiter.SpringExtension; - -@ExtendWith(SpringExtension.class) -@SpringBootTest -class BuckPalApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/io/reflectoring/buckpal/DependencyRuleTests.java b/src/test/java/io/reflectoring/buckpal/DependencyRuleTests.java deleted file mode 100644 index 08f81bd..0000000 --- a/src/test/java/io/reflectoring/buckpal/DependencyRuleTests.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.reflectoring.buckpal; - -import com.tngtech.archunit.core.importer.ClassFileImporter; -import io.reflectoring.buckpal.archunit.HexagonalArchitecture; -import org.junit.jupiter.api.Test; - -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; - -class DependencyRuleTests { - - @Test - void validateRegistrationContextArchitecture() { - HexagonalArchitecture.basePackage("io.reflectoring.buckpal") - - .withDomainLayer("application.domain") - - .withAdaptersLayer("adapter") - .incoming("in.web") - .outgoing("out.persistence") - .and() - - .withApplicationLayer("application") - .incomingPorts("port.in") - .outgoingPorts("port.out") - .and() - - .withConfiguration("configuration") - .check(new ClassFileImporter() - .importPackages("io.reflectoring.buckpal..")); - } - - @Test - void domainModelDoesNotDependOnOutside() { - noClasses() - .that() - .resideInAPackage("io.reflectoring.buckpal.application.domain.model..") - .should() - .dependOnClassesThat() - .resideOutsideOfPackages( - "io.reflectoring.buckpal.application.domain.model..", - "lombok..", - "java.." - ) - .check(new ClassFileImporter() - .importPackages("io.reflectoring.buckpal..")); - } - -} diff --git a/src/test/java/io/reflectoring/buckpal/SendMoneySystemTest.java b/src/test/java/io/reflectoring/buckpal/SendMoneySystemTest.java deleted file mode 100644 index b664d9c..0000000 --- a/src/test/java/io/reflectoring/buckpal/SendMoneySystemTest.java +++ /dev/null @@ -1,104 +0,0 @@ -package io.reflectoring.buckpal; - -import java.time.LocalDateTime; - -import io.reflectoring.buckpal.application.port.out.LoadAccountPort; -import io.reflectoring.buckpal.application.domain.model.Account; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.Money; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.client.TestRestTemplate; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.test.context.jdbc.Sql; -import static org.assertj.core.api.BDDAssertions.*; - -@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) -class SendMoneySystemTest { - - @Autowired - private TestRestTemplate restTemplate; - - @Autowired - private LoadAccountPort loadAccountPort; - - @Test - @Sql("SendMoneySystemTest.sql") - void sendMoney() { - - Money initialSourceBalance = sourceAccount().calculateBalance(); - Money initialTargetBalance = targetAccount().calculateBalance(); - - ResponseEntity response = whenSendMoney( - sourceAccountId(), - targetAccountId(), - transferredAmount()); - - then(response.getStatusCode()) - .isEqualTo(HttpStatus.OK); - - then(sourceAccount().calculateBalance()) - .isEqualTo(initialSourceBalance.minus(transferredAmount())); - - then(targetAccount().calculateBalance()) - .isEqualTo(initialTargetBalance.plus(transferredAmount())); - - } - - private Account sourceAccount() { - return loadAccount(sourceAccountId()); - } - - private Account targetAccount() { - return loadAccount(targetAccountId()); - } - - private Account loadAccount(AccountId accountId) { - return loadAccountPort.loadAccount( - accountId, - LocalDateTime.now()); - } - - - private ResponseEntity whenSendMoney( - AccountId sourceAccountId, - AccountId targetAccountId, - Money amount) { - HttpHeaders headers = new HttpHeaders(); - headers.add("Content-Type", "application/json"); - HttpEntity request = new HttpEntity<>(null, headers); - - return restTemplate.exchange( - "/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}", - HttpMethod.POST, - request, - Object.class, - sourceAccountId.getValue(), - targetAccountId.getValue(), - amount.getAmount()); - } - - private Money transferredAmount() { - return Money.of(500L); - } - - private Money balanceOf(AccountId accountId) { - Account account = loadAccountPort.loadAccount(accountId, LocalDateTime.now()); - return account.calculateBalance(); - } - - private AccountId sourceAccountId() { - return new AccountId(1L); - } - - private AccountId targetAccountId() { - return new AccountId(2L); - } - -} diff --git a/src/test/java/io/reflectoring/buckpal/adapter/in/web/SendMoneyControllerTest.java b/src/test/java/io/reflectoring/buckpal/adapter/in/web/SendMoneyControllerTest.java deleted file mode 100644 index 1b0bd3c..0000000 --- a/src/test/java/io/reflectoring/buckpal/adapter/in/web/SendMoneyControllerTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package io.reflectoring.buckpal.adapter.in.web; - -import io.reflectoring.buckpal.application.port.in.SendMoneyUseCase; -import io.reflectoring.buckpal.application.port.in.SendMoneyCommand; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.Money; -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.test.web.servlet.MockMvc; -import static org.mockito.BDDMockito.*; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@WebMvcTest(controllers = SendMoneyController.class) -class SendMoneyControllerTest { - - @Autowired - private MockMvc mockMvc; - - @MockBean - private SendMoneyUseCase sendMoneyUseCase; - - @Test - void testSendMoney() throws Exception { - - mockMvc.perform(post("/accounts/send/{sourceAccountId}/{targetAccountId}/{amount}", - 41L, 42L, 500) - .header("Content-Type", "application/json")) - .andExpect(status().isOk()); - - then(sendMoneyUseCase).should() - .sendMoney(eq(new SendMoneyCommand( - new AccountId(41L), - new AccountId(42L), - Money.of(500L)))); - } - -} diff --git a/src/test/java/io/reflectoring/buckpal/adapter/out/persistence/AccountPersistenceAdapterTest.java b/src/test/java/io/reflectoring/buckpal/adapter/out/persistence/AccountPersistenceAdapterTest.java deleted file mode 100644 index 0a4510f..0000000 --- a/src/test/java/io/reflectoring/buckpal/adapter/out/persistence/AccountPersistenceAdapterTest.java +++ /dev/null @@ -1,55 +0,0 @@ -package io.reflectoring.buckpal.adapter.out.persistence; - -import java.time.LocalDateTime; - -import io.reflectoring.buckpal.application.domain.model.Account; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.ActivityWindow; -import io.reflectoring.buckpal.application.domain.model.Money; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.jdbc.Sql; -import static io.reflectoring.buckpal.common.AccountTestData.*; -import static io.reflectoring.buckpal.common.ActivityTestData.*; -import static org.assertj.core.api.Assertions.assertThat; - -@DataJpaTest -@Import({AccountPersistenceAdapter.class, AccountMapper.class}) -class AccountPersistenceAdapterTest { - - @Autowired - private AccountPersistenceAdapter adapterUnderTest; - - @Autowired - private ActivityRepository activityRepository; - - @Test - @Sql("AccountPersistenceAdapterTest.sql") - void loadsAccount() { - Account account = adapterUnderTest.loadAccount(new AccountId(1L), LocalDateTime.of(2018, 8, 10, 0, 0)); - - assertThat(account.getActivityWindow().getActivities()).hasSize(2); - assertThat(account.calculateBalance()).isEqualTo(Money.of(500)); - } - - @Test - void updatesActivities() { - Account account = defaultAccount() - .withBaselineBalance(Money.of(555L)) - .withActivityWindow(new ActivityWindow( - defaultActivity() - .withId(null) - .withMoney(Money.of(1L)).build())) - .build(); - - adapterUnderTest.updateActivities(account); - - assertThat(activityRepository.count()).isEqualTo(1); - - ActivityJpaEntity savedActivity = activityRepository.findAll().get(0); - assertThat(savedActivity.getAmount()).isEqualTo(1L); - } - -} \ No newline at end of file diff --git a/src/test/java/io/reflectoring/buckpal/application/domain/AccountTest.java b/src/test/java/io/reflectoring/buckpal/application/domain/AccountTest.java deleted file mode 100644 index c6de162..0000000 --- a/src/test/java/io/reflectoring/buckpal/application/domain/AccountTest.java +++ /dev/null @@ -1,101 +0,0 @@ -package io.reflectoring.buckpal.application.domain; - -import io.reflectoring.buckpal.application.domain.model.Account; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.ActivityWindow; -import io.reflectoring.buckpal.application.domain.model.Money; -import org.junit.jupiter.api.Test; -import static io.reflectoring.buckpal.common.AccountTestData.*; -import static io.reflectoring.buckpal.common.ActivityTestData.*; -import static org.assertj.core.api.Assertions.*; - -class AccountTest { - - @Test - void calculatesBalance() { - AccountId accountId = new AccountId(1L); - Account account = defaultAccount() - .withAccountId(accountId) - .withBaselineBalance(Money.of(555L)) - .withActivityWindow(new ActivityWindow( - defaultActivity() - .withTargetAccount(accountId) - .withMoney(Money.of(999L)).build(), - defaultActivity() - .withTargetAccount(accountId) - .withMoney(Money.of(1L)).build())) - .build(); - - Money balance = account.calculateBalance(); - - assertThat(balance).isEqualTo(Money.of(1555L)); - } - - @Test - void withdrawalSucceeds() { - AccountId accountId = new AccountId(1L); - Account account = defaultAccount() - .withAccountId(accountId) - .withBaselineBalance(Money.of(555L)) - .withActivityWindow(new ActivityWindow( - defaultActivity() - .withTargetAccount(accountId) - .withMoney(Money.of(999L)).build(), - defaultActivity() - .withTargetAccount(accountId) - .withMoney(Money.of(1L)).build())) - .build(); - - AccountId randomTargetAccount = new AccountId(99L); - boolean success = account.withdraw(Money.of(555L), randomTargetAccount); - - assertThat(success).isTrue(); - assertThat(account.getActivityWindow().getActivities()).hasSize(3); - assertThat(account.calculateBalance()).isEqualTo(Money.of(1000L)); - } - - @Test - void withdrawalFailure() { - AccountId accountId = new AccountId(1L); - Account account = defaultAccount() - .withAccountId(accountId) - .withBaselineBalance(Money.of(555L)) - .withActivityWindow(new ActivityWindow( - defaultActivity() - .withTargetAccount(accountId) - .withMoney(Money.of(999L)).build(), - defaultActivity() - .withTargetAccount(accountId) - .withMoney(Money.of(1L)).build())) - .build(); - - boolean success = account.withdraw(Money.of(1556L), new AccountId(99L)); - - assertThat(success).isFalse(); - assertThat(account.getActivityWindow().getActivities()).hasSize(2); - assertThat(account.calculateBalance()).isEqualTo(Money.of(1555L)); - } - - @Test - void depositSuccess() { - AccountId accountId = new AccountId(1L); - Account account = defaultAccount() - .withAccountId(accountId) - .withBaselineBalance(Money.of(555L)) - .withActivityWindow(new ActivityWindow( - defaultActivity() - .withTargetAccount(accountId) - .withMoney(Money.of(999L)).build(), - defaultActivity() - .withTargetAccount(accountId) - .withMoney(Money.of(1L)).build())) - .build(); - - boolean success = account.deposit(Money.of(445L), new AccountId(99L)); - - assertThat(success).isTrue(); - assertThat(account.getActivityWindow().getActivities()).hasSize(3); - assertThat(account.calculateBalance()).isEqualTo(Money.of(2000L)); - } - -} \ No newline at end of file diff --git a/src/test/java/io/reflectoring/buckpal/application/domain/ActivityWindowTest.java b/src/test/java/io/reflectoring/buckpal/application/domain/ActivityWindowTest.java deleted file mode 100644 index 601019c..0000000 --- a/src/test/java/io/reflectoring/buckpal/application/domain/ActivityWindowTest.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.reflectoring.buckpal.application.domain; - -import java.time.LocalDateTime; - -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.ActivityWindow; -import io.reflectoring.buckpal.application.domain.model.Money; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; -import static io.reflectoring.buckpal.common.ActivityTestData.*; - -class ActivityWindowTest { - - @Test - void calculatesStartTimestamp() { - ActivityWindow window = new ActivityWindow( - defaultActivity().withTimestamp(startDate()).build(), - defaultActivity().withTimestamp(inBetweenDate()).build(), - defaultActivity().withTimestamp(endDate()).build()); - - Assertions.assertThat(window.getStartTimestamp()).isEqualTo(startDate()); - } - - @Test - void calculatesEndTimestamp() { - ActivityWindow window = new ActivityWindow( - defaultActivity().withTimestamp(startDate()).build(), - defaultActivity().withTimestamp(inBetweenDate()).build(), - defaultActivity().withTimestamp(endDate()).build()); - - Assertions.assertThat(window.getEndTimestamp()).isEqualTo(endDate()); - } - - @Test - void calculatesBalance() { - - AccountId account1 = new AccountId(1L); - AccountId account2 = new AccountId(2L); - - ActivityWindow window = new ActivityWindow( - defaultActivity() - .withSourceAccount(account1) - .withTargetAccount(account2) - .withMoney(Money.of(999)).build(), - defaultActivity() - .withSourceAccount(account1) - .withTargetAccount(account2) - .withMoney(Money.of(1)).build(), - defaultActivity() - .withSourceAccount(account2) - .withTargetAccount(account1) - .withMoney(Money.of(500)).build()); - - Assertions.assertThat(window.calculateBalance(account1)).isEqualTo(Money.of(-500)); - Assertions.assertThat(window.calculateBalance(account2)).isEqualTo(Money.of(500)); - } - - private LocalDateTime startDate() { - return LocalDateTime.of(2019, 8, 3, 0, 0); - } - - private LocalDateTime inBetweenDate() { - return LocalDateTime.of(2019, 8, 4, 0, 0); - } - - private LocalDateTime endDate() { - return LocalDateTime.of(2019, 8, 5, 0, 0); - } - -} \ No newline at end of file diff --git a/src/test/java/io/reflectoring/buckpal/application/domain/service/SendMoneyServiceTest.java b/src/test/java/io/reflectoring/buckpal/application/domain/service/SendMoneyServiceTest.java deleted file mode 100644 index c899eb5..0000000 --- a/src/test/java/io/reflectoring/buckpal/application/domain/service/SendMoneyServiceTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package io.reflectoring.buckpal.application.domain.service; - -import io.reflectoring.buckpal.application.port.in.SendMoneyCommand; -import io.reflectoring.buckpal.application.port.out.AccountLock; -import io.reflectoring.buckpal.application.port.out.LoadAccountPort; -import io.reflectoring.buckpal.application.port.out.UpdateAccountStatePort; -import io.reflectoring.buckpal.application.domain.model.Account; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.Money; -import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; - -import static org.assertj.core.api.Assertions.*; -import static org.mockito.BDDMockito.*; - -class SendMoneyServiceTest { - - private final LoadAccountPort loadAccountPort = - Mockito.mock(LoadAccountPort.class); - - private final AccountLock accountLock = - Mockito.mock(AccountLock.class); - - private final UpdateAccountStatePort updateAccountStatePort = - Mockito.mock(UpdateAccountStatePort.class); - - private final SendMoneyService sendMoneyService = - new SendMoneyService(loadAccountPort, accountLock, updateAccountStatePort, moneyTransferProperties()); - - @Test - void givenWithdrawalFails_thenOnlySourceAccountIsLockedAndReleased() { - - AccountId sourceAccountId = new AccountId(41L); - Account sourceAccount = givenAnAccountWithId(sourceAccountId); - - AccountId targetAccountId = new AccountId(42L); - Account targetAccount = givenAnAccountWithId(targetAccountId); - - givenWithdrawalWillFail(sourceAccount); - givenDepositWillSucceed(targetAccount); - - SendMoneyCommand command = new SendMoneyCommand( - sourceAccountId, - targetAccountId, - Money.of(300L)); - - boolean success = sendMoneyService.sendMoney(command); - - assertThat(success).isFalse(); - - then(accountLock).should().lockAccount(eq(sourceAccountId)); - then(accountLock).should().releaseAccount(eq(sourceAccountId)); - then(accountLock).should(times(0)).lockAccount(eq(targetAccountId)); - } - - @Test - void transactionSucceeds() { - - Account sourceAccount = givenSourceAccount(); - Account targetAccount = givenTargetAccount(); - - givenWithdrawalWillSucceed(sourceAccount); - givenDepositWillSucceed(targetAccount); - - Money money = Money.of(500L); - - SendMoneyCommand command = new SendMoneyCommand( - sourceAccount.getId().get(), - targetAccount.getId().get(), - money); - - boolean success = sendMoneyService.sendMoney(command); - - assertThat(success).isTrue(); - - AccountId sourceAccountId = sourceAccount.getId().get(); - AccountId targetAccountId = targetAccount.getId().get(); - - then(accountLock).should().lockAccount(eq(sourceAccountId)); - then(sourceAccount).should().withdraw(eq(money), eq(targetAccountId)); - then(accountLock).should().releaseAccount(eq(sourceAccountId)); - - then(accountLock).should().lockAccount(eq(targetAccountId)); - then(targetAccount).should().deposit(eq(money), eq(sourceAccountId)); - then(accountLock).should().releaseAccount(eq(targetAccountId)); - - thenAccountsHaveBeenUpdated(sourceAccountId, targetAccountId); - } - - private void thenAccountsHaveBeenUpdated(AccountId... accountIds){ - ArgumentCaptor accountCaptor = ArgumentCaptor.forClass(Account.class); - then(updateAccountStatePort).should(times(accountIds.length)) - .updateActivities(accountCaptor.capture()); - - List updatedAccountIds = accountCaptor.getAllValues() - .stream() - .map(Account::getId) - .map(Optional::get) - .collect(Collectors.toList()); - - for(AccountId accountId : accountIds){ - assertThat(updatedAccountIds).contains(accountId); - } - } - - private void givenDepositWillSucceed(Account account) { - given(account.deposit(any(Money.class), any(AccountId.class))) - .willReturn(true); - } - - private void givenWithdrawalWillFail(Account account) { - given(account.withdraw(any(Money.class), any(AccountId.class))) - .willReturn(false); - } - - private void givenWithdrawalWillSucceed(Account account) { - given(account.withdraw(any(Money.class), any(AccountId.class))) - .willReturn(true); - } - - private Account givenTargetAccount(){ - return givenAnAccountWithId(new AccountId(42L)); - } - - private Account givenSourceAccount(){ - return givenAnAccountWithId(new AccountId(41L)); - } - - private Account givenAnAccountWithId(AccountId id) { - Account account = Mockito.mock(Account.class); - given(account.getId()) - .willReturn(Optional.of(id)); - given(loadAccountPort.loadAccount(eq(account.getId().get()), any(LocalDateTime.class))) - .willReturn(account); - return account; - } - - private MoneyTransferProperties moneyTransferProperties(){ - return new MoneyTransferProperties(Money.of(Long.MAX_VALUE)); - } - -} diff --git a/src/test/java/io/reflectoring/buckpal/application/port/in/SendMoneyCommandTest.java b/src/test/java/io/reflectoring/buckpal/application/port/in/SendMoneyCommandTest.java deleted file mode 100644 index 3ecfea8..0000000 --- a/src/test/java/io/reflectoring/buckpal/application/port/in/SendMoneyCommandTest.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.reflectoring.buckpal.application.port.in; - -import io.reflectoring.buckpal.application.domain.model.Account; -import io.reflectoring.buckpal.application.domain.model.Money; -import jakarta.validation.ConstraintViolationException; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.math.BigInteger; - -class SendMoneyCommandTest { - - @Test - public void validationOk() { - new SendMoneyCommand( - new Account.AccountId(42L), - new Account.AccountId(43L), - new Money(new BigInteger("10"))); - // no exception - } - - @Test - public void moneyValidationFails() { - Assertions.assertThrows(ConstraintViolationException.class, () -> { - new SendMoneyCommand( - new Account.AccountId(42L), - new Account.AccountId(43L), - new Money(new BigInteger("-10"))); - }); - } - - @Test - public void accountIdValidationFails() { - Assertions.assertThrows(ConstraintViolationException.class, () -> { - new SendMoneyCommand( - new Account.AccountId(42L), - null, - new Money(new BigInteger("10"))); - }); - } - -} \ No newline at end of file diff --git a/src/test/java/io/reflectoring/buckpal/archunit/Adapters.java b/src/test/java/io/reflectoring/buckpal/archunit/Adapters.java deleted file mode 100644 index 4bff0e7..0000000 --- a/src/test/java/io/reflectoring/buckpal/archunit/Adapters.java +++ /dev/null @@ -1,62 +0,0 @@ -package io.reflectoring.buckpal.archunit; - -import java.util.ArrayList; -import java.util.List; - -import com.tngtech.archunit.core.domain.JavaClasses; - -public class Adapters extends ArchitectureElement { - - private final HexagonalArchitecture parentContext; - private List incomingAdapterPackages = new ArrayList<>(); - private List outgoingAdapterPackages = new ArrayList<>(); - - Adapters(HexagonalArchitecture parentContext, String basePackage) { - super(basePackage); - this.parentContext = parentContext; - } - - public Adapters outgoing(String packageName) { - this.incomingAdapterPackages.add(fullQualifiedPackage(packageName)); - return this; - } - - public Adapters incoming(String packageName) { - this.outgoingAdapterPackages.add(fullQualifiedPackage(packageName)); - return this; - } - - List allAdapterPackages() { - List allAdapters = new ArrayList<>(); - allAdapters.addAll(incomingAdapterPackages); - allAdapters.addAll(outgoingAdapterPackages); - return allAdapters; - } - - public HexagonalArchitecture and() { - return parentContext; - } - - String getBasePackage() { - return basePackage; - } - - void dontDependOnEachOther(JavaClasses classes) { - List allAdapters = allAdapterPackages(); - for (String adapter1 : allAdapters) { - for (String adapter2 : allAdapters) { - if (!adapter1.equals(adapter2)) { - denyDependency(adapter1, adapter2, classes); - } - } - } - } - - void doesNotDependOn(String packageName, JavaClasses classes) { - denyDependency(this.basePackage, packageName, classes); - } - - void doesNotContainEmptyPackages() { - denyEmptyPackages(allAdapterPackages()); - } -} diff --git a/src/test/java/io/reflectoring/buckpal/archunit/ApplicationLayer.java b/src/test/java/io/reflectoring/buckpal/archunit/ApplicationLayer.java deleted file mode 100644 index 7c4613a..0000000 --- a/src/test/java/io/reflectoring/buckpal/archunit/ApplicationLayer.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.reflectoring.buckpal.archunit; - -import java.util.ArrayList; -import java.util.List; - -import com.tngtech.archunit.core.domain.JavaClasses; - -public class ApplicationLayer extends ArchitectureElement { - - private final HexagonalArchitecture parentContext; - private List incomingPortsPackages = new ArrayList<>(); - private List outgoingPortsPackages = new ArrayList<>(); - private List servicePackages = new ArrayList<>(); - - public ApplicationLayer(String basePackage, HexagonalArchitecture parentContext) { - super(basePackage); - this.parentContext = parentContext; - } - - public ApplicationLayer incomingPorts(String packageName) { - this.incomingPortsPackages.add(fullQualifiedPackage(packageName)); - return this; - } - - public ApplicationLayer outgoingPorts(String packageName) { - this.outgoingPortsPackages.add(fullQualifiedPackage(packageName)); - return this; - } - - public ApplicationLayer services(String packageName) { - this.servicePackages.add(fullQualifiedPackage(packageName)); - return this; - } - - public HexagonalArchitecture and() { - return parentContext; - } - - public void doesNotDependOn(String packageName, JavaClasses classes) { - denyDependency(this.basePackage, packageName, classes); - } - - public void incomingAndOutgoingPortsDoNotDependOnEachOther(JavaClasses classes) { - denyAnyDependency(this.incomingPortsPackages, this.outgoingPortsPackages, classes); - denyAnyDependency(this.outgoingPortsPackages, this.incomingPortsPackages, classes); - } - - private List allPackages() { - List allPackages = new ArrayList<>(); - allPackages.addAll(incomingPortsPackages); - allPackages.addAll(outgoingPortsPackages); - allPackages.addAll(servicePackages); - return allPackages; - } - - void doesNotContainEmptyPackages() { - denyEmptyPackages(allPackages()); - } -} diff --git a/src/test/java/io/reflectoring/buckpal/archunit/ArchitectureElement.java b/src/test/java/io/reflectoring/buckpal/archunit/ArchitectureElement.java deleted file mode 100644 index 1861b82..0000000 --- a/src/test/java/io/reflectoring/buckpal/archunit/ArchitectureElement.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.reflectoring.buckpal.archunit; - -import java.util.List; - -import com.tngtech.archunit.core.domain.JavaClasses; -import com.tngtech.archunit.core.importer.ClassFileImporter; -import static com.tngtech.archunit.base.DescribedPredicate.*; -import static com.tngtech.archunit.lang.conditions.ArchConditions.*; -import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*; - -abstract class ArchitectureElement { - - final String basePackage; - - public ArchitectureElement(String basePackage) { - this.basePackage = basePackage; - } - - String fullQualifiedPackage(String relativePackage) { - return this.basePackage + "." + relativePackage; - } - - static void denyDependency(String fromPackageName, String toPackageName, JavaClasses classes) { - noClasses() - .that() - .resideInAPackage(matchAllClassesInPackage(fromPackageName)) - .should() - .dependOnClassesThat() - .resideInAnyPackage(matchAllClassesInPackage(toPackageName)) - .check(classes); - } - - static void denyAnyDependency( - List fromPackages, List toPackages, JavaClasses classes) { - for (String fromPackage : fromPackages) { - for (String toPackage : toPackages) { - denyDependency(fromPackage, toPackage, classes); - } - } - } - - static String matchAllClassesInPackage(String packageName) { - return packageName + ".."; - } - - void denyEmptyPackage(String packageName) { - classes() - .that() - .resideInAPackage(matchAllClassesInPackage(packageName)) - .should(containNumberOfElements(greaterThanOrEqualTo(1))) - .check(classesInPackage(packageName)); - } - - private JavaClasses classesInPackage(String packageName) { - return new ClassFileImporter().importPackages(packageName); - } - - void denyEmptyPackages(List packages) { - for (String packageName : packages) { - denyEmptyPackage(packageName); - } - } -} diff --git a/src/test/java/io/reflectoring/buckpal/archunit/HexagonalArchitecture.java b/src/test/java/io/reflectoring/buckpal/archunit/HexagonalArchitecture.java deleted file mode 100644 index e3cad8a..0000000 --- a/src/test/java/io/reflectoring/buckpal/archunit/HexagonalArchitecture.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.reflectoring.buckpal.archunit; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import com.tngtech.archunit.core.domain.JavaClasses; - -public class HexagonalArchitecture extends ArchitectureElement { - - private Adapters adapters; - private ApplicationLayer applicationLayer; - private String configurationPackage; - private List domainPackages = new ArrayList<>(); - - public static HexagonalArchitecture basePackage(String basePackage) { - return new HexagonalArchitecture(basePackage); - } - - public HexagonalArchitecture(String basePackage) { - super(basePackage); - } - - public Adapters withAdaptersLayer(String adaptersPackage) { - this.adapters = new Adapters(this, fullQualifiedPackage(adaptersPackage)); - return this.adapters; - } - - public HexagonalArchitecture withDomainLayer(String domainPackage) { - this.domainPackages.add(fullQualifiedPackage(domainPackage)); - return this; - } - - public ApplicationLayer withApplicationLayer(String applicationPackage) { - this.applicationLayer = new ApplicationLayer(fullQualifiedPackage(applicationPackage), this); - return this.applicationLayer; - } - - public HexagonalArchitecture withConfiguration(String packageName) { - this.configurationPackage = fullQualifiedPackage(packageName); - return this; - } - - private void domainDoesNotDependOnAdapters(JavaClasses classes) { - denyAnyDependency( - this.domainPackages, Collections.singletonList(adapters.basePackage), classes); - } - - public void check(JavaClasses classes) { - this.adapters.doesNotContainEmptyPackages(); - this.adapters.dontDependOnEachOther(classes); - this.adapters.doesNotDependOn(this.configurationPackage, classes); - this.applicationLayer.doesNotContainEmptyPackages(); - this.applicationLayer.doesNotDependOn(this.adapters.getBasePackage(), classes); - this.applicationLayer.doesNotDependOn(this.configurationPackage, classes); - this.applicationLayer.incomingAndOutgoingPortsDoNotDependOnEachOther(classes); - this.domainDoesNotDependOnAdapters(classes); - } -} diff --git a/src/test/java/io/reflectoring/buckpal/common/AccountTestData.java b/src/test/java/io/reflectoring/buckpal/common/AccountTestData.java deleted file mode 100644 index b06ff6f..0000000 --- a/src/test/java/io/reflectoring/buckpal/common/AccountTestData.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.reflectoring.buckpal.common; - -import io.reflectoring.buckpal.application.domain.model.Account; -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.ActivityWindow; -import io.reflectoring.buckpal.application.domain.model.Money; - -public class AccountTestData { - - public static AccountBuilder defaultAccount() { - return new AccountBuilder() - .withAccountId(new AccountId(42L)) - .withBaselineBalance(Money.of(999L)) - .withActivityWindow(new ActivityWindow( - ActivityTestData.defaultActivity().build(), - ActivityTestData.defaultActivity().build())); - } - - - public static class AccountBuilder { - - private AccountId accountId; - private Money baselineBalance; - private ActivityWindow activityWindow; - - public AccountBuilder withAccountId(AccountId accountId) { - this.accountId = accountId; - return this; - } - - public AccountBuilder withBaselineBalance(Money baselineBalance) { - this.baselineBalance = baselineBalance; - return this; - } - - public AccountBuilder withActivityWindow(ActivityWindow activityWindow) { - this.activityWindow = activityWindow; - return this; - } - - public Account build() { - return Account.withId(this.accountId, this.baselineBalance, this.activityWindow); - } - - } - - -} diff --git a/src/test/java/io/reflectoring/buckpal/common/ActivityTestData.java b/src/test/java/io/reflectoring/buckpal/common/ActivityTestData.java deleted file mode 100644 index 6ba52f8..0000000 --- a/src/test/java/io/reflectoring/buckpal/common/ActivityTestData.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.reflectoring.buckpal.common; - -import java.time.LocalDateTime; - -import io.reflectoring.buckpal.application.domain.model.Account.AccountId; -import io.reflectoring.buckpal.application.domain.model.Activity; -import io.reflectoring.buckpal.application.domain.model.Activity.ActivityId; -import io.reflectoring.buckpal.application.domain.model.Money; - -public class ActivityTestData { - - public static ActivityBuilder defaultActivity(){ - return new ActivityBuilder() - .withOwnerAccount(new AccountId(42L)) - .withSourceAccount(new AccountId(42L)) - .withTargetAccount(new AccountId(41L)) - .withTimestamp(LocalDateTime.now()) - .withMoney(Money.of(999L)); - } - - public static class ActivityBuilder { - private ActivityId id; - private AccountId ownerAccountId; - private AccountId sourceAccountId; - private AccountId targetAccountId; - private LocalDateTime timestamp; - private Money money; - - public ActivityBuilder withId(ActivityId id) { - this.id = id; - return this; - } - - public ActivityBuilder withOwnerAccount(AccountId accountId) { - this.ownerAccountId = accountId; - return this; - } - - public ActivityBuilder withSourceAccount(AccountId accountId) { - this.sourceAccountId = accountId; - return this; - } - - public ActivityBuilder withTargetAccount(AccountId accountId) { - this.targetAccountId = accountId; - return this; - } - - public ActivityBuilder withTimestamp(LocalDateTime timestamp) { - this.timestamp = timestamp; - return this; - } - - public ActivityBuilder withMoney(Money money) { - this.money = money; - return this; - } - - public Activity build() { - return new Activity( - this.id, - this.ownerAccountId, - this.sourceAccountId, - this.targetAccountId, - this.timestamp, - this.money); - } - } -} diff --git a/src/test/resources/io/reflectoring/buckpal/SendMoneySystemTest.sql b/src/test/resources/io/reflectoring/buckpal/SendMoneySystemTest.sql deleted file mode 100644 index 7973596..0000000 --- a/src/test/resources/io/reflectoring/buckpal/SendMoneySystemTest.sql +++ /dev/null @@ -1,26 +0,0 @@ -insert into account (id) values (1); -insert into account (id) values (2); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (1001, '2018-08-08 08:00:00.0', 1, 1, 2, 500); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (1002, '2018-08-08 08:00:00.0', 2, 1, 2, 500); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (1003, '2018-08-09 10:00:00.0', 1, 2, 1, 1000); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (1004, '2018-08-09 10:00:00.0', 2, 2, 1, 1000); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (1005, '2019-08-09 09:00:00.0', 1, 1, 2, 1000); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (1006, '2019-08-09 09:00:00.0', 2, 1, 2, 1000); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (1007, '2019-08-09 10:00:00.0', 1, 2, 1, 1000); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (1008, '2019-08-09 10:00:00.0', 2, 2, 1, 1000); \ No newline at end of file diff --git a/src/test/resources/io/reflectoring/buckpal/adapter/out/persistence/AccountPersistenceAdapterTest.sql b/src/test/resources/io/reflectoring/buckpal/adapter/out/persistence/AccountPersistenceAdapterTest.sql deleted file mode 100644 index 3698724..0000000 --- a/src/test/resources/io/reflectoring/buckpal/adapter/out/persistence/AccountPersistenceAdapterTest.sql +++ /dev/null @@ -1,26 +0,0 @@ -insert into account (id) values (1); -insert into account (id) values (2); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (1, '2018-08-08 08:00:00.0', 1, 1, 2, 500); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (2, '2018-08-08 08:00:00.0', 2, 1, 2, 500); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (3, '2018-08-09 10:00:00.0', 1, 2, 1, 1000); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (4, '2018-08-09 10:00:00.0', 2, 2, 1, 1000); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (5, '2019-08-09 09:00:00.0', 1, 1, 2, 1000); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (6, '2019-08-09 09:00:00.0', 2, 1, 2, 1000); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (7, '2019-08-09 10:00:00.0', 1, 2, 1, 1000); - -insert into activity (id, timestamp, owner_account_id, source_account_id, target_account_id, amount) -values (8, '2019-08-09 10:00:00.0', 2, 2, 1, 1000); \ No newline at end of file