Compare commits

..

48 Commits

Author SHA1 Message Date
Wojtek Krzywiec
6dc153309b add spock reports 2020-10-23 07:58:22 +02:00
Wojtek Krzywiec
3218fb056e small changes 2020-10-20 07:05:30 +02:00
Wojtek Krzywiec
8306b5158a move stubed services to a tests class 2020-10-14 06:42:34 +02:00
Wojtek Krzywiec
967a349b4c bring back JUnit4 deps 2020-10-13 06:34:26 +02:00
Wojtek Krzywiec
42b08c8995 BorrowingFacadeSpec added 2020-09-20 12:32:04 +02:00
Wojtek Krzywiec
58a9a0f95b add spock dependencies to pom.xml 2020-09-19 12:41:34 +02:00
Wojtek Krzywiec
9b0c37ffd7 Merge branch 'polishing' 2020-06-14 15:09:45 +02:00
Wojtek Krzywiec
53939113f2 fixes 2020-06-14 15:08:53 +02:00
Wojtek Krzywiec
a9fd37ec5a Merge pull request #18 from wkrzywiec/polishing
Polishing
2020-06-08 21:33:59 +02:00
wkrzywiec
18d1038a18 rename user table to library_user plus small type corrections 2020-06-08 21:31:32 +02:00
Wojtek Krzywiec
f071d557ff more restful controller 2020-06-08 07:07:26 +02:00
Wojtek Krzywiec
b7db201e7b database helper for tests 2020-06-08 06:41:30 +02:00
Wojtek Krzywiec
36316de3f7 database init for testing 2020-06-08 06:15:10 +02:00
Wojtek Krzywiec
428d281e38 actuator added & spring profiles introduced 2020-06-07 23:13:32 +02:00
Wojtek Krzywiec
62960ef444 Merge pull request #17 from wkrzywiec/give-back-book
Give back book
2020-06-07 11:51:13 +02:00
Wojtek Krzywiec
19ce80db76 add RowMapper for ReservedBook and BorrowedBook 2020-06-07 11:47:57 +02:00
Wojtek Krzywiec
8a1e7ae20e give back application layer 2020-06-07 11:03:28 +02:00
Wojtek Krzywiec
d2b7d08072 add give back business logic 2020-06-07 08:56:17 +02:00
Wojtek Krzywiec
2e5858d8a9 add give back business logic 2020-06-05 22:41:12 +02:00
Wojtek Krzywiec
5e221dd427 Merge pull request #16 from wkrzywiec/borrow
Borrow book
2020-06-04 08:18:13 +02:00
Wojtek Krzywiec
e5e4767a9c applicationa layer for borrowing books 2020-06-04 08:15:48 +02:00
Wojtek Krzywiec
8dc64f2c79 adpater for saving borrowed book 2020-06-04 06:59:03 +02:00
Wojtek Krzywiec
ba246cf0c9 borrow book facade 2020-06-02 07:52:52 +02:00
Wojtek Krzywiec
a557dea9a9 cleaning 2020-05-31 16:19:56 +02:00
Wojtek Krzywiec
b2f9c7f56f Merge pull request #15 from wkrzywiec/cancel-reservation
cancel reservation scheduler added
2020-05-31 16:03:51 +02:00
Wojtek Krzywiec
c13e60f42b correct test coverage exclusions 2020-05-31 15:58:21 +02:00
Wojtek Krzywiec
58978def54 cancel reservation scheduler added 2020-05-30 21:57:35 +02:00
Wojtek Krzywiec
6b0aeb4141 Merge pull request #14 from wkrzywiec/cancel-reservation
Cancel reservation
2020-05-30 21:50:09 +02:00
Wojtek Krzywiec
273dfed81e refactoring 2020-05-30 21:47:27 +02:00
Wojtek Krzywiec
62c683fb63 find overdue reservations 2020-05-30 21:30:56 +02:00
Wojtek Krzywiec
943693bd1d cancel reservation core 2020-05-30 16:35:25 +02:00
Wojtek Krzywiec
dbe12c918f remove book from available table after reservation 2020-05-29 20:37:54 +02:00
Wojtek Krzywiec
6f4c938a85 Merge pull request #13 from wkrzywiec/user
User domain
2020-05-29 17:45:12 +02:00
wkrzywiec
4d69df25e9 UserTestData and BookTestData 2020-05-29 17:40:47 +02:00
wkrzywiec
fa0e83ad92 UserCommandController 2020-05-29 08:56:16 +02:00
wkrzywiec
93f2eed4c8 UserDatabaseAdapter implemented 2020-05-28 20:20:45 +02:00
wkrzywiec
b2b1003b0b add user domain 2020-05-28 13:24:33 +02:00
Wojtek Krzywiec
29f3997a98 Merge pull request #12 from wkrzywiec/reservation-email
Reservation email
2020-05-28 11:52:54 +02:00
wkrzywiec
727481a520 ignore model classes for test coverage 2020-05-28 11:49:51 +02:00
wkrzywiec
5f4809cfd4 get book title & email address from db - email domain 2020-05-28 10:57:26 +02:00
wkrzywiec
c8b8c26d80 implement email sending adapter 2020-05-28 10:03:24 +02:00
wkrzywiec
9e5ba16a1c clean database after each integration test 2020-05-27 18:25:13 +02:00
wkrzywiec
3f07eac529 email domain tests 2020-05-25 08:35:00 +02:00
wkrzywiec
a234301f1d email address creation tests 2020-05-25 08:11:41 +02:00
wkrzywiec
14441bfbd6 email domain logic 2020-05-25 07:11:01 +02:00
wkrzywiec
648b877b86 publish book reserved event 2020-05-25 06:28:42 +02:00
Wojtek Krzywiec
fbeac054cd Merge pull request #11 from wkrzywiec/add-new-book-event
database cleanup
2020-05-24 12:05:09 +02:00
Wojtek Krzywiec
c9de6efc28 Merge pull request #10 from wkrzywiec/add-new-book-event
Add new book event
2020-05-24 11:53:55 +02:00
158 changed files with 3037 additions and 1091 deletions

View File

@@ -8,4 +8,4 @@ RUN mvn -B -f pom.xml clean package -DskipTests
FROM openjdk:11-jdk-slim
COPY --from=build /workspace/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]
ENTRYPOINT ["java","-jar","app.jar", "--spring.profiles.active=prod"]

View File

@@ -19,14 +19,23 @@ In the terminal run the following command:
$ docker-compose up
```
#### Using Maven
#### Using Maven (with H2 or local Postgres database)
First make sure that you adjust the configuration file - `src/main/resources/application.yml` with connection details to your database.
First compile an application:
Then, in the terminal run the following command:
```console
$ mvn clean package
$ mvn spring-boot:run
```
Then, you have two options either run it with H2 database or with local Postgres database. For first approach just run:
```console
$ mvn spring-boot:run
```
For a second option, check in the configuration file - `src/main/resources/application.yml` for profile *local-postgres* if connection details are correct and if so, run the command:
```console
$ mvn spring-boot:run -P local-postgres
```
#### Inside IntelliJ (with H2 or Postgres database)

98
pom.xml
View File

@@ -6,7 +6,7 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
<relativePath/>
</parent>
<groupId>io.wkrzywiec.hexagonal</groupId>
@@ -17,11 +17,14 @@
<properties>
<java.version>11</java.version>
<spring-cloud.version>Hoxton.M3</spring-cloud.version>
<surefire.and.failsafe.report.dir>target/test-report</surefire.and.failsafe.report.dir>
<code.coverage.exclusions>
**/io/wkrzywiec/hexagonal/library/domain/book/dto/**,
**/io/wkrzywiec/hexagonal/library/infrastructure/repository/*Entity.java
**/io/wkrzywiec/hexagonal/library/domain/borrowing/core/model/**,
**/io/wkrzywiec/hexagonal/library/domain/email/core/model/**,
**/io/wkrzywiec/hexagonal/library/domain/inventory/core/model/**,
**/io/wkrzywiec/hexagonal/library/domain/user/core/model/**,
**/io/wkrzywiec/hexagonal/library/**/*Entity.java,
**/io/wkrzywiec/hexagonal/library/LibraryHexagonalApplication.java
</code.coverage.exclusions>
</properties>
@@ -30,6 +33,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
@@ -55,11 +62,6 @@
<artifactId>jackson-module-jaxb-annotations</artifactId>
<version>2.11.0</version>
</dependency>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.3</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
@@ -89,27 +91,48 @@
<version>1.2.32</version>
</dependency>
<!-- TEST dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-core</artifactId>
<version>1.3-groovy-2.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.spockframework</groupId>
<artifactId>spock-spring</artifactId>
<version>1.3-groovy-2.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.athaydes</groupId>
<artifactId>spock-reports</artifactId>
<version>1.8.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>0.13.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
</dependency>
</dependencies>
<build>
@@ -142,8 +165,25 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<reportsDirectory>${surefire.and.failsafe.report.dir}</reportsDirectory>
<includes>
<include>**/*Spec.java</include>
<include>**/*Test.java</include>
</includes>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.gmavenplus</groupId>
<artifactId>gmavenplus-plugin</artifactId>
<version>1.10.0</version>
<executions>
<execution>
<goals>
<goal>addTestSources</goal>
<goal>compileTests</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
@@ -188,6 +228,33 @@
</build>
<profiles>
<profile>
<id>default</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<properties>
<active-profiles>default</active-profiles>
</properties>
</profile>
<profile>
<id>prod</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<properties>
<active-profiles>prod</active-profiles>
</properties>
</profile>
<profile>
<id>local-postgres</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<properties>
<active-profiles>local-postgres</active-profiles>
</properties>
</profile>
<profile>
<id>component-test</id>
<build>
@@ -234,6 +301,7 @@
<sonar.sources>.</sonar.sources>
<sonar.inclusions>src/main/java/**,src/main/resources/**</sonar.inclusions>
<sonar.exclusions>${code.coverage.exclusions}</sonar.exclusions>
<sonat.tests>src/test/groovy,src/test/java</sonat.tests>
<sonar.projectKey>wkrzywiec_library-hexagonal</sonar.projectKey>
<sonar.organization>wkrzywiec</sonar.organization>
<sonar.coverage.jacoco.xmlReportPaths>target/site/jacoco/jacoco.xml,target/site/jacoco-it/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>

View File

@@ -1,42 +0,0 @@
package io.wkrzywiec.hexagonal.library;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Table;
import java.util.List;
import java.util.stream.Collectors;
//https://gist.github.com/JorgenRingen/a56837fc07e630c32280b8e3d14c2d24
@Service
public class DatabaseCleanup implements InitializingBean {
@PersistenceContext
private EntityManager entityManager;
private List<String> tableNames;
@Transactional
public void execute() {
entityManager.flush();
entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY FALSE").executeUpdate();
for (final String tableName : tableNames) {
entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
}
entityManager.createNativeQuery("SET REFERENTIAL_INTEGRITY TRUE").executeUpdate();
}
@Override
public void afterPropertiesSet() throws Exception {
tableNames = entityManager.getMetamodel().getEntities().stream()
.filter(e -> e.getJavaType().getAnnotation(Table.class) != null)
.map(e -> e.getJavaType().getAnnotation(Table.class).name())
.collect(Collectors.toList());
}
}

View File

@@ -1,96 +0,0 @@
package io.wkrzywiec.hexagonal.library.borrowing;
import io.wkrzywiec.hexagonal.library.DatabaseCleanup;
import io.wkrzywiec.hexagonal.library.TestData;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.inventory.infrastructure.BookRepository;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.jdbc.core.JdbcTemplate;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MakeReservationComponentTest {
@LocalServerPort
private int port;
@Autowired
private BookRepository bookRepository;
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private DatabaseCleanup databaseCleanup;
private String baseURL;
@BeforeEach
public void init() {
this.baseURL = "http://localhost:" + port;
Book book = bookRepository.save(TestData.homoDeusBook());
jdbcTemplate.update(
"INSERT INTO available (book_id) VALUES (?)",
book.getIdAsLong());
jdbcTemplate.update(
"INSERT INTO user (first_name, last_name, email) VALUES (?, ?, ?)",
"John",
"Doe",
"john.doe@test.com");
}
@AfterEach
public void after() {
databaseCleanup.execute();
}
@Test
@DisplayName("Reserve available book")
public void givenBookIsAvailable_thenMakeReservation_thenBookIsReserved() {
//given
Long homoDeusBookId = jdbcTemplate.queryForObject(
"SELECT id FROM book WHERE title = ?",
Long.class,
TestData.homoDeusBookTitle());
Long activeUserId = jdbcTemplate.queryForObject(
"SELECT id FROM user WHERE email = ?",
Long.class,
"john.doe@test.com");
BookReservationCommand reservationCommand =
BookReservationCommand.builder()
.bookId(homoDeusBookId )
.userId(activeUserId)
.build();
//when
given()
.contentType("application/json")
.body(reservationCommand)
.when()
.post( baseURL + "/reservations")
.prettyPeek()
.then();
Long reservationId = jdbcTemplate.queryForObject(
"SELECT id FROM reserved WHERE book_id = ?",
Long.class,
homoDeusBookId);
assertTrue(reservationId > 0);
}
}

View File

@@ -0,0 +1,28 @@
package io.wkrzywiec.hexagonal.library.domain;
import io.restassured.RestAssured;
import io.wkrzywiec.hexagonal.library.DatabaseHelper;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.jdbc.core.JdbcTemplate;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public abstract class BaseComponentTest {
@LocalServerPort
private int port;
protected String baseURL;
@Autowired
protected JdbcTemplate jdbcTemplate;
protected DatabaseHelper databaseHelper;
@BeforeEach
public void init() {
this.baseURL = "http://localhost:" + port;
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
databaseHelper = new DatabaseHelper(jdbcTemplate);
}
}

View File

@@ -0,0 +1,44 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing;
import io.wkrzywiec.hexagonal.library.domain.BaseComponentTest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.BookStatus;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.ChangeBookStatusRequest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowBookCommand;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.jdbc.Sql;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
public class BorrowBookComponentTest extends BaseComponentTest {
@Test
@DisplayName("Borrow reserved book")
@Sql({"/book-and-user.sql", "/reserved-book.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void givenBookIsReserved_thenBorrowIt_thenBookIsBorrowed() {
//given
Long homoDeusBookId = databaseHelper.getHomoDeusBookId();
Long activeUserId = databaseHelper.getJohnDoeUserId();
ChangeBookStatusRequest borrowRequest =
ChangeBookStatusRequest.builder()
.userId(activeUserId)
.status(BookStatus.BORROWED)
.build();
//when
given()
.contentType("application/json")
.body(borrowRequest)
.when()
.patch( baseURL + "/books/" + homoDeusBookId + "/status")
.prettyPeek()
.then();
Long borrowId = databaseHelper.getPrimaryKeyOfBorrowedByBookId(homoDeusBookId);
assertTrue(borrowId > 0);
}
}

View File

@@ -0,0 +1,45 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing;
import io.wkrzywiec.hexagonal.library.domain.BaseComponentTest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.BookStatus;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.ChangeBookStatusRequest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.GiveBackBookCommand;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.jdbc.Sql;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
public class GiveBackBookComponentTest extends BaseComponentTest {
@Test
@DisplayName("Give back borrowed book")
@Sql({"/book-and-user.sql", "/borrowed-book.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void givenBookIsBorrowed_thenGiveBackIt_thenItIsAvailable() {
//given
Long homoDeusBookId = databaseHelper.getHomoDeusBookId();
Long activeUserId = databaseHelper.getJohnDoeUserId();
ChangeBookStatusRequest giveBackRequest =
ChangeBookStatusRequest.builder()
.userId(activeUserId)
.status(BookStatus.AVAILABLE)
.build();
//when
given()
.contentType("application/json")
.body(giveBackRequest)
.when()
.patch( baseURL + "/books/" + homoDeusBookId + "/status")
.prettyPeek()
.then();
Long bookId = databaseHelper.getPrimaryKeyOfAvailableByBookBy(homoDeusBookId);
assertEquals(homoDeusBookId, bookId);
}
}

View File

@@ -0,0 +1,49 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing;
import io.wkrzywiec.hexagonal.library.domain.BaseComponentTest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.BookStatus;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.ChangeBookStatusRequest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.domain.inventory.infrastructure.BookRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.jdbc.Sql;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
public class MakeReservationComponentTest extends BaseComponentTest {
@Autowired
private BookRepository bookRepository;
@Test
@DisplayName("Reserve available book")
@Sql({"/book-and-user.sql", "/available-book.sql"})
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void givenBookIsAvailable_thenMakeReservation_thenBookIsReserved() {
//given
Long homoDeusBookId = databaseHelper.getHomoDeusBookId();
Long activeUserId = databaseHelper.getJohnDoeUserId();
ChangeBookStatusRequest reservationRequest =
ChangeBookStatusRequest.builder()
.userId(activeUserId)
.status(BookStatus.RESERVED)
.build();
//when
given()
.contentType("application/json")
.body(reservationRequest)
.when()
.patch( baseURL + "/books/" + homoDeusBookId + "/status")
.prettyPeek()
.then();
Long reservationId = databaseHelper.getPrimaryKeyOfReservationByBookId(homoDeusBookId);
assertTrue(reservationId > 0);
}
}

View File

@@ -0,0 +1,63 @@
package io.wkrzywiec.hexagonal.library.domain.inventory;
import io.restassured.response.ValidatableResponse;
import io.wkrzywiec.hexagonal.library.BookTestData;
import io.wkrzywiec.hexagonal.library.domain.BaseComponentTest;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.AddNewBookCommand;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpStatus;
import org.springframework.test.context.jdbc.Sql;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
public class AddNewBookComponentTest extends BaseComponentTest {
@Test
@DisplayName("Search for a new book in Google Books")
public void whenSearchForBook_thenGetList(){
//when
ValidatableResponse response = given()
.when()
.param("query", "lean startup")
.get( baseURL + "/google/books")
.prettyPeek()
.then();
//then
response.statusCode(HttpStatus.OK.value())
.contentType("application/json")
.body("items.size()", greaterThan(0));
}
@Test
@DisplayName("Add new book to a database & make it available")
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void givenGoogleBooId_whenAddNewBook_thenBookIsSaved() {
//given
AddNewBookCommand addNewBookCommand =
AddNewBookCommand.builder()
.googleBookId(BookTestData.homoDeusBookGoogleId())
.build();
//when
given()
.contentType("application/json")
.body(addNewBookCommand)
.when()
.post( baseURL + "/books")
.prettyPeek()
.then();
//then
Long savedBookId = databaseHelper.getHomoDeusBookId();
assertTrue(savedBookId > 0);
Long availableBookId = databaseHelper.getPrimaryKeyOfAvailableByBookBy(savedBookId);
assertTrue(availableBookId > 0);
}
}

View File

@@ -0,0 +1,40 @@
package io.wkrzywiec.hexagonal.library.domain.user;
import io.wkrzywiec.hexagonal.library.domain.BaseComponentTest;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.AddUserCommand;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.jdbc.Sql;
import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.springframework.test.context.jdbc.Sql.ExecutionPhase.AFTER_TEST_METHOD;
public class AddNewUserComponentTest extends BaseComponentTest {
@Test
@DisplayName("Create new user")
@Sql(scripts = "/clean-database.sql", executionPhase = AFTER_TEST_METHOD)
public void shouldCreateNewUser(){
//given
AddUserCommand addUserCommand = AddUserCommand.builder()
.firstName("John")
.lastName("Doe")
.email("john.doe@test.com")
.build();
//when
given()
.contentType("application/json")
.body(addUserCommand)
.when()
.post( baseURL + "/users")
.prettyPeek()
.then();
//then
Long savedUserId = databaseHelper.getJohnDoeUserId();
assertTrue(savedUserId > 0);
}
}

View File

@@ -1,97 +0,0 @@
package io.wkrzywiec.hexagonal.library.inventory;
import io.restassured.RestAssured;
import io.restassured.response.ValidatableResponse;
import io.wkrzywiec.hexagonal.library.DatabaseCleanup;
import io.wkrzywiec.hexagonal.library.TestData;
import io.wkrzywiec.hexagonal.library.inventory.model.AddNewBookCommand;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.greaterThan;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class AddNewBookComponentTest {
@LocalServerPort
private int port;
@Autowired
private JdbcTemplate jdbc;
@Autowired
private DatabaseCleanup databaseCleanup;
private String baseURL;
@BeforeEach
public void init(){
this.baseURL = "http://localhost:" + port;
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
@AfterEach
public void after() {
databaseCleanup.execute();
}
@Test
@DisplayName("Search for a new book in Google Books")
public void whenSearchForBook_thenGetList(){
//when
ValidatableResponse response = given()
.when()
.param("query", "lean startup")
.get( baseURL + "/google/books")
.prettyPeek()
.then();
//then
response.statusCode(HttpStatus.OK.value())
.contentType("application/json")
.body("items.size()", greaterThan(0));
}
@Test
@DisplayName("Add new book to a database & make it available")
public void givenGoogleBooId_whenAddNewBook_thenBookIsSaved() {
//given
AddNewBookCommand addNewBookCommand =
AddNewBookCommand.builder()
.googleBookId(TestData.homoDeusBookGoogleId())
.build();
//when
given()
.contentType("application/json")
.body(addNewBookCommand)
.when()
.post( baseURL + "/books")
.prettyPeek()
.then();
//then
Long savedBookId = jdbc.queryForObject(
"SELECT id FROM book WHERE book_external_id = ?",
Long.class,
TestData.homoDeusBookGoogleId());
assertTrue(savedBookId > 0);
Long availableBookId = jdbc.queryForObject(
"SELECT id FROM available WHERE book_id = ?",
Long.class,
savedBookId);
assertTrue(availableBookId > 0);
}
}

View File

@@ -4,4 +4,4 @@ spring:
driverClassName: org.h2.Driver
username: sa
password: password
jpa.database-platform: org.hibernate.dialect.H2Dialect
jpa.database-platform: org.hibernate.dialect.H2Dialect

View File

@@ -1,23 +0,0 @@
package io.wkrzywiec.hexagonal.library;
import io.wkrzywiec.hexagonal.library.borrowing.BorrowingFacade;
import io.wkrzywiec.hexagonal.library.borrowing.infrastructure.BorrowingDatabaseAdapter;
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class BorrowingDomainConfig {
@Bean
public BorrowingDatabase borrowingDatabase(JdbcTemplate jdbcTemplate) {
return new BorrowingDatabaseAdapter(jdbcTemplate);
}
@Bean
public MakeBookAvailable makeBookAvailable(BorrowingDatabase database) {
return new BorrowingFacade(database);
}
}

View File

@@ -1,9 +1,24 @@
package io.wkrzywiec.hexagonal.library;
import io.wkrzywiec.hexagonal.library.infrastructure.BorrowingDomainConfig;
import io.wkrzywiec.hexagonal.library.infrastructure.EmailDomainConfig;
import io.wkrzywiec.hexagonal.library.infrastructure.InventoryDomainConfig;
import io.wkrzywiec.hexagonal.library.infrastructure.LibraryHexagonalConfig;
import io.wkrzywiec.hexagonal.library.infrastructure.UserDomainConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@Import({
LibraryHexagonalConfig.class,
InventoryDomainConfig.class,
BorrowingDomainConfig.class,
EmailDomainConfig.class,
UserDomainConfig.class
})
public class LibraryHexagonalApplication {
public static void main(String[] args) {

View File

@@ -0,0 +1,15 @@
package io.wkrzywiec.hexagonal.library.application;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/")
public class LibraryHexagonalController {
@GetMapping("")
public String getAppRoot(){
return "Library Hexagonal REST API";
}
}

View File

@@ -1,42 +0,0 @@
package io.wkrzywiec.hexagonal.library.borrowing;
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
import io.wkrzywiec.hexagonal.library.borrowing.model.MakeBookAvailableCommand;
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.ActiveUserNotFoundException;
import io.wkrzywiec.hexagonal.library.borrowing.model.AvailableBook;
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.AvailableBookNotFoundExeption;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservedBook;
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.ReserveBook;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
public class BorrowingFacade implements MakeBookAvailable, ReserveBook {
private BorrowingDatabase database;
public BorrowingFacade(BorrowingDatabase database) {
this.database = database;
}
@Override
public void handle(MakeBookAvailableCommand bookAvailableCommand) {
database.setBookAvailable(bookAvailableCommand.getBookId());
}
@Override
public Long handle(BookReservationCommand bookReservation) {
AvailableBook availableBook =
database.getAvailableBook(bookReservation.getBookId())
.orElseThrow(() -> new AvailableBookNotFoundExeption(bookReservation.getBookId()));
ActiveUser activeUser =
database.getActiveUser(bookReservation.getUserId())
.orElseThrow(() -> new ActiveUserNotFoundException(bookReservation.getUserId()));
ReservedBook reservedBook = activeUser.reserve(availableBook);
ReservationDetails reservationDetails = database.save(reservedBook);
return reservationDetails.getReservationId().getIdAsLong();
}
}

View File

@@ -1,25 +0,0 @@
package io.wkrzywiec.hexagonal.library.borrowing.application;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.ReserveBook;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/reservations")
@RequiredArgsConstructor
public class ReservationController {
private final ReserveBook reserveBook;
@PostMapping("")
public ResponseEntity<String> makeReservation(@RequestBody BookReservationCommand reservationCommand){
Long reservationId = reserveBook.handle(reservationCommand);
return new ResponseEntity<>("Reservation has been made with an id " + reservationId, HttpStatus.CREATED);
}
}

View File

@@ -1,82 +0,0 @@
package io.wkrzywiec.hexagonal.library.borrowing.infrastructure;
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
import io.wkrzywiec.hexagonal.library.borrowing.model.AvailableBook;
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationId;
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservedBook;
import io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing.BorrowingDatabase;
import lombok.AllArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
@AllArgsConstructor
public class BorrowingDatabaseAdapter implements BorrowingDatabase {
private JdbcTemplate jdbcTemplate;
@Override
public void setBookAvailable(Long bookId) {
jdbcTemplate.update(
"INSERT INTO available (book_id) VALUES (?)",
bookId);
}
@Override
public Optional<AvailableBook> getAvailableBook(Long bookId) {
try {
return Optional.ofNullable(
jdbcTemplate.queryForObject(
"SELECT book_id FROM available WHERE book_id = ?",
AvailableBook.class,
bookId));
} catch (DataAccessException exception) {
return Optional.empty();
}
}
@Override
public Optional<ActiveUser> getActiveUser(Long userId) {
try {
jdbcTemplate.queryForObject(
"SELECT id FROM public.user as u WHERE u.id = ?",
Long.class,
userId);
} catch (DataAccessException exception) {
return Optional.empty();
}
List<ReservedBook> reservedBooksByUser = getReservedBooksByUser(userId);
return Optional.of(new ActiveUser(userId, reservedBooksByUser));
}
private List<ReservedBook> getReservedBooksByUser(Long userId) {
try {
return jdbcTemplate.queryForList(
"SELECT book_id FROM reserved WHERE reserved.user_id = ?",
ReservedBook.class,
userId
);
} catch (DataAccessException exception){
return new ArrayList<>();
}
}
@Override
public ReservationDetails save(ReservedBook reservedBook) {
jdbcTemplate.update(
"INSERT INTO reserved (book_id, user_id) VALUES (?, ?)",
reservedBook.getIdAsLong(),
reservedBook.getAssignedUserIdAsLong());
ReservationId reservationId = jdbcTemplate.queryForObject(
"SELECT id FROM reserved WHERE book_id = ?",
ReservationId.class,
reservedBook.getIdAsLong());
return new ReservationDetails(reservationId, reservedBook);
}
}

View File

@@ -1,36 +0,0 @@
package io.wkrzywiec.hexagonal.library.borrowing.model;
import io.wkrzywiec.hexagonal.library.borrowing.model.exception.TooManyBooksAssignedToUserException;
import lombok.EqualsAndHashCode;
import java.util.List;
@EqualsAndHashCode
public class ActiveUser {
private final Long id;
private final List<ReservedBook> reservedBooks;
public ActiveUser(Long id, List<ReservedBook> reservedBooks) {
this.id = id;
this.reservedBooks = reservedBooks;
}
public ReservedBook reserve(AvailableBook availableBook){
if (reservedBooks.size() < 3){
ReservedBook reservedBook = new ReservedBook(availableBook.getIdAsLong(), id);
reservedBooks.add(reservedBook);
return reservedBook;
} else {
throw new TooManyBooksAssignedToUserException(id);
}
}
public Long getIdAsLong(){
return id;
}
public List<ReservedBook> getReservedBookList(){
return reservedBooks;
}
}

View File

@@ -1,5 +0,0 @@
package io.wkrzywiec.hexagonal.library.borrowing.model;
interface Book {
Long getIdAsLong();
}

View File

@@ -1,24 +0,0 @@
package io.wkrzywiec.hexagonal.library.borrowing.model;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode
public class ReservedBook implements Book {
private final Long bookId;
private final Long userId;
public ReservedBook(Long bookId, Long userId) {
this.bookId = bookId;
this.userId = userId;
}
@Override
public Long getIdAsLong() {
return bookId;
}
public Long getAssignedUserIdAsLong(){
return userId;
}
}

View File

@@ -1,7 +0,0 @@
package io.wkrzywiec.hexagonal.library.borrowing.ports.incoming;
import io.wkrzywiec.hexagonal.library.borrowing.model.MakeBookAvailableCommand;
public interface MakeBookAvailable {
void handle(MakeBookAvailableCommand bookAvailableCommand);
}

View File

@@ -1,7 +0,0 @@
package io.wkrzywiec.hexagonal.library.borrowing.ports.incoming;
import io.wkrzywiec.hexagonal.library.borrowing.model.BookReservationCommand;
public interface ReserveBook {
Long handle(BookReservationCommand bookReservation);
}

View File

@@ -1,15 +0,0 @@
package io.wkrzywiec.hexagonal.library.borrowing.ports.outgoing;
import io.wkrzywiec.hexagonal.library.borrowing.model.ActiveUser;
import io.wkrzywiec.hexagonal.library.borrowing.model.AvailableBook;
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.borrowing.model.ReservedBook;
import java.util.Optional;
public interface BorrowingDatabase {
void setBookAvailable(Long bookId);
Optional<AvailableBook> getAvailableBook(Long bookId);
Optional<ActiveUser> getActiveUser(Long userId);
ReservationDetails save(ReservedBook reservedBook);
}

View File

@@ -0,0 +1,50 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.application;
import io.wkrzywiec.hexagonal.library.domain.borrowing.application.model.ChangeBookStatusRequest;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.GiveBackBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.BorrowBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.GiveBackBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.ReserveBook;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/books")
@RequiredArgsConstructor
public class BorrowingDomainController {
@Qualifier("GiveBackBook")
private final GiveBackBook giveBackBook;
@Qualifier("ReserveBook")
private final ReserveBook reserveBook;
@Qualifier("BorrowBook")
private final BorrowBook borrowBook;
@PatchMapping("/{id}/status")
public ResponseEntity<String> borrowBook(@PathVariable("id") Long bookId, @RequestBody ChangeBookStatusRequest request){
switch (request.getStatus()){
case AVAILABLE:
giveBackBook.handle(new GiveBackBookCommand(bookId, request.getUserId()));
return new ResponseEntity<>("Book with an id " + bookId + " was returned", HttpStatus.OK);
case RESERVED:
Long reservationId = reserveBook.handle(new BookReservationCommand(bookId, request.getUserId()));
return new ResponseEntity<>("Reservation has been made with an id " + reservationId, HttpStatus.OK);
case BORROWED:
borrowBook.handle(new BorrowBookCommand(bookId, request.getUserId()));
return new ResponseEntity<>("Book with an id " + bookId + " was borrowed", HttpStatus.OK);
default:
return new ResponseEntity<>("Book can't have status: " + request.getStatus(), HttpStatus.BAD_REQUEST);
}
}
}

View File

@@ -1,9 +1,10 @@
package io.wkrzywiec.hexagonal.library.borrowing.application;
package io.wkrzywiec.hexagonal.library.domain.borrowing.application;
import io.wkrzywiec.hexagonal.library.borrowing.model.MakeBookAvailableCommand;
import io.wkrzywiec.hexagonal.library.borrowing.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.MakeBookAvailableCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.NewBookWasAddedEvent;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@@ -11,6 +12,7 @@ import org.springframework.stereotype.Component;
@Component
public class NewBookWasAddedEventHandler {
@Qualifier("MakeBookAvailable")
private final MakeBookAvailable makeBookAvailable;
@EventListener

View File

@@ -0,0 +1,18 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.application;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.CancelOverdueReservations;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.scheduling.annotation.Scheduled;
@RequiredArgsConstructor
public class OverdueReservationScheduler {
@Qualifier("CancelOverdueReservations")
private final CancelOverdueReservations overdueReservations;
@Scheduled(fixedRate = 60 * 1000)
public void checkOverdueReservations(){
overdueReservations.cancelOverdueReservations();
}
}

View File

@@ -0,0 +1,5 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.application.model;
public enum BookStatus {
AVAILABLE, RESERVED, BORROWED
}

View File

@@ -0,0 +1,12 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.application.model;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class ChangeBookStatusRequest {
private BookStatus status;
private Long userId;
}

View File

@@ -0,0 +1,91 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ActiveUser;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.AvailableBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservationCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservedEvent;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.GiveBackBookCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.MakeBookAvailableCommand;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.OverdueReservation;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.ActiveUserNotFoundException;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.AvailableBookNotFoundExeption;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.BorrowedBookNotFoundException;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.ReservedBookNotFoundException;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.BorrowBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.CancelOverdueReservations;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.GiveBackBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.MakeBookAvailable;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming.ReserveBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingDatabase;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingEventPublisher;
import java.util.List;
public class BorrowingFacade implements MakeBookAvailable, ReserveBook, CancelOverdueReservations, BorrowBook, GiveBackBook {
private final BorrowingDatabase database;
private final BorrowingEventPublisher eventPublisher;
public BorrowingFacade(BorrowingDatabase database, BorrowingEventPublisher eventPublisher) {
this.database = database;
this.eventPublisher = eventPublisher;
}
@Override
public void handle(MakeBookAvailableCommand bookAvailableCommand) {
database.save(new AvailableBook(bookAvailableCommand.getBookId()));
}
@Override
public Long handle(BookReservationCommand bookReservation) {
AvailableBook availableBook =
database.getAvailableBook(bookReservation.getBookId())
.orElseThrow(() -> new AvailableBookNotFoundExeption(bookReservation.getBookId()));
ActiveUser activeUser =
database.getActiveUser(bookReservation.getUserId())
.orElseThrow(() -> new ActiveUserNotFoundException(bookReservation.getUserId()));
ReservedBook reservedBook = activeUser.reserve(availableBook);
ReservationDetails reservationDetails = database.save(reservedBook);
eventPublisher.publish(new BookReservedEvent(reservationDetails));
return reservationDetails.getReservationId().getIdAsLong();
}
@Override
public void cancelOverdueReservations() {
List<OverdueReservation> overdueReservationList = database.findReservationsForMoreThan(3L);
overdueReservationList.forEach(
overdueBook -> database.save(new AvailableBook(overdueBook.getBookIdentificationAsLong())));
}
@Override
public void handle(BorrowBookCommand borrowBookCommand) {
ActiveUser activeUser =
database.getActiveUser(borrowBookCommand.getUserId())
.orElseThrow(() -> new ActiveUserNotFoundException(borrowBookCommand.getUserId()));
ReservedBook reservedBook =
database.getReservedBook(borrowBookCommand.getBookId())
.orElseThrow(() -> new ReservedBookNotFoundException(borrowBookCommand.getBookId()));
BorrowedBook borrowedBook = activeUser.borrow(reservedBook);
database.save(borrowedBook);
}
@Override
public void handle(GiveBackBookCommand command) {
BorrowedBook borrowedBook =
database.getBorrowedBook(command.getBookId())
.orElseThrow(() -> new BorrowedBookNotFoundException(command.getBookId()));
ActiveUser activeUser =
database.getActiveUser(command.getUserId())
.orElseThrow(() -> new ActiveUserNotFoundException(command.getUserId()));
AvailableBook availableBook = activeUser.giveBack(borrowedBook);
database.save(availableBook);
}
}

View File

@@ -0,0 +1,69 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception.TooManyBooksAssignedToUserException;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.util.LinkedHashMap;
import java.util.List;
@EqualsAndHashCode
@ToString
public class ActiveUser {
private final Long id;
private final List<ReservedBook> reservedBooks;
private final List<BorrowedBook> borrowedBooks;
public ActiveUser(Long id, List<ReservedBook> reservedBooks, List<BorrowedBook> borrowedBooks) {
this.id = id;
this.reservedBooks = reservedBooks;
this.borrowedBooks = borrowedBooks;
}
public ReservedBook reserve(AvailableBook availableBook){
if (hasUserNotReachedLimitOfBooks()){
ReservedBook reservedBook = new ReservedBook(availableBook.getIdAsLong(), id);
reservedBooks.add(reservedBook);
return reservedBook;
} else {
throw new TooManyBooksAssignedToUserException(id);
}
}
public BorrowedBook borrow(ReservedBook reservedBook) {
if (hasUserNotReachedLimitOfBooks()){
BorrowedBook borrowedBook = new BorrowedBook(reservedBook.getIdAsLong(), id);
borrowedBooks.add(borrowedBook);
return borrowedBook;
} else {
throw new TooManyBooksAssignedToUserException(id);
}
}
public AvailableBook giveBack(BorrowedBook borrowedBook) {
boolean isBookRemovedFromUserAccount = borrowedBooks.removeIf(book -> book.equals(borrowedBook));
if (isBookRemovedFromUserAccount){
return new AvailableBook(borrowedBook.getIdAsLong());
} else {
throw new IllegalArgumentException("User with an id: " + id + " didn't borrow book with an id: " + borrowedBook.getIdAsLong());
}
}
public Long getIdAsLong(){
return id;
}
public List<ReservedBook> getReservedBookList(){
return reservedBooks;
}
public List<BorrowedBook> getBorrowedBookList() {
return borrowedBooks;
}
private boolean hasUserNotReachedLimitOfBooks(){
return reservedBooks.size() + borrowedBooks.size() < 3;
}
}

View File

@@ -1,8 +1,10 @@
package io.wkrzywiec.hexagonal.library.borrowing.model;
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@EqualsAndHashCode
@ToString
public class AvailableBook implements Book {
private final Long id;

View File

@@ -0,0 +1,5 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
interface Book {
Long getIdAsLong();
}

View File

@@ -1,10 +1,12 @@
package io.wkrzywiec.hexagonal.library.borrowing.model;
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class BookReservationCommand {

View File

@@ -0,0 +1,35 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import java.time.Instant;
public class BookReservedEvent {
private final ReservationId reservationId;
private final Long userId;
private final ReservedBook reservedBook;
private final Instant timestamp;
public BookReservedEvent(ReservationDetails reservationDetails) {
this.reservationId = reservationDetails.getReservationId();
this.userId = reservationDetails.getReservedBook().getAssignedUserIdAsLong();
this.reservedBook = reservationDetails.getReservedBook();
timestamp = Instant.now();
}
public Long getReservationIdAsLong() {
return reservationId.getIdAsLong();
}
public Long getUserIdAsLong() {
return userId;
}
public Long getBookIdAsLong() {
return reservedBook.getIdAsLong();
}
public String getEventTimeStampAsString() {
return timestamp.toString();
}
}

View File

@@ -0,0 +1,13 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@AllArgsConstructor
@Getter
@Builder
public class BorrowBookCommand {
private Long bookId;
private Long userId;
}

View File

@@ -0,0 +1,39 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.EqualsAndHashCode;
import java.time.Instant;
@EqualsAndHashCode
public class BorrowedBook implements Book {
private Long bookId;
private Long userId;
@EqualsAndHashCode.Exclude
private Instant borrowedDate;
public BorrowedBook(Long bookId, Long userId) {
this.bookId = bookId;
this.userId = userId;
this.borrowedDate = Instant.now();
}
public BorrowedBook(Long bookId, Long userId, Instant borrowedDate) {
this.bookId = bookId;
this.userId = userId;
this.borrowedDate = borrowedDate;
}
@Override
public Long getIdAsLong() {
return bookId;
}
public Long getAssignedUserIdAsLong(){
return userId;
}
public Instant getBorrowedDateAsInstant(){
return borrowedDate;
}
}

View File

@@ -0,0 +1,14 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.AllArgsConstructor;
import java.time.Instant;
@AllArgsConstructor
public class DueDate {
private final Instant timeStamp;
public Instant asInstant(){
return timeStamp;
}
}

View File

@@ -0,0 +1,13 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
@AllArgsConstructor
@Getter
@Builder
public class GiveBackBookCommand {
private Long bookId;
private Long userId;
}

View File

@@ -1,10 +1,8 @@
package io.wkrzywiec.hexagonal.library.borrowing.model;
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@Getter

View File

@@ -0,0 +1,13 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class OverdueReservation {
private Long reservationId;
private Long bookIdentification;
public Long getBookIdentificationAsLong() {
return bookIdentification;
}
}

View File

@@ -1,4 +1,4 @@
package io.wkrzywiec.hexagonal.library.borrowing.model;
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.EqualsAndHashCode;
import lombok.Getter;

View File

@@ -1,4 +1,4 @@
package io.wkrzywiec.hexagonal.library.borrowing.model;
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.EqualsAndHashCode;

View File

@@ -0,0 +1,39 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.time.Instant;
@EqualsAndHashCode
public class ReservedBook implements Book {
private final Long bookId;
private final Long userId;
private final Instant reservedDate;
public ReservedBook(Long bookId, Long userId) {
this.bookId = bookId;
this.userId = userId;
this.reservedDate = Instant.now();
}
public ReservedBook(Long bookId, Long userId, Instant reservedDate) {
this.bookId = bookId;
this.userId = userId;
this.reservedDate = reservedDate;
}
@Override
public Long getIdAsLong() {
return bookId;
}
public Long getAssignedUserIdAsLong(){
return userId;
}
public Instant getReservedDateAsInstant(){
return reservedDate;
}
}

View File

@@ -1,4 +1,4 @@
package io.wkrzywiec.hexagonal.library.borrowing.model.exception;
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception;
public class ActiveUserNotFoundException extends RuntimeException {
public ActiveUserNotFoundException(Long bookId){

View File

@@ -1,4 +1,4 @@
package io.wkrzywiec.hexagonal.library.borrowing.model.exception;
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception;
public class AvailableBookNotFoundExeption extends RuntimeException {
public AvailableBookNotFoundExeption(Long bookId){

View File

@@ -0,0 +1,10 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception;
public class BorrowedBookNotFoundException extends RuntimeException {
public BorrowedBookNotFoundException(Long bookId) {
super("There is no borrowed book with an ID: " + bookId,
null,
false,
false);
}
}

View File

@@ -0,0 +1,10 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception;
public class ReservedBookNotFoundException extends RuntimeException {
public ReservedBookNotFoundException(Long bookId) {
super("There is no reserved book with an ID: " + bookId,
null,
false,
false);
}
}

View File

@@ -1,4 +1,4 @@
package io.wkrzywiec.hexagonal.library.borrowing.model.exception;
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.exception;
public class TooManyBooksAssignedToUserException extends RuntimeException {
public TooManyBooksAssignedToUserException(Long userId){

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowBookCommand;
public interface BorrowBook {
void handle(BorrowBookCommand borrowBookCommand);
}

View File

@@ -0,0 +1,5 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming;
public interface CancelOverdueReservations {
void cancelOverdueReservations();
}

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.GiveBackBookCommand;
public interface GiveBackBook {
void handle(GiveBackBookCommand command);
}

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.MakeBookAvailableCommand;
public interface MakeBookAvailable {
void handle(MakeBookAvailableCommand bookAvailableCommand);
}

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.incoming;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservationCommand;
public interface ReserveBook {
Long handle(BookReservationCommand bookReservation);
}

View File

@@ -0,0 +1,23 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ActiveUser;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.AvailableBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.DueDate;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.OverdueReservation;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservedBook;
import java.util.List;
import java.util.Optional;
public interface BorrowingDatabase {
void save(AvailableBook availableBook);
ReservationDetails save(ReservedBook reservedBook);
void save(BorrowedBook borrowedBook);
Optional<AvailableBook> getAvailableBook(Long bookId);
Optional<ActiveUser> getActiveUser(Long userId);
List<OverdueReservation> findReservationsForMoreThan(Long days);
Optional<ReservedBook> getReservedBook(Long bookId);
Optional<BorrowedBook> getBorrowedBook(Long bookId);
}

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservedEvent;
public interface BorrowingEventPublisher {
public void publish(BookReservedEvent event);
}

View File

@@ -0,0 +1,172 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure.entity.OverdueReservationEntity;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ActiveUser;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.AvailableBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.DueDate;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.OverdueReservation;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationDetails;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservationId;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservedBook;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingDatabase;
import io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure.mapper.BorrowedBookRowMapper;
import io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure.mapper.ReservedBookRowMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@RequiredArgsConstructor
public class BorrowingDatabaseAdapter implements BorrowingDatabase {
private final JdbcTemplate jdbcTemplate;
@Override
public void save(AvailableBook availableBook) {
jdbcTemplate.update(
"INSERT INTO available (book_id) VALUES (?)",
availableBook.getIdAsLong());
jdbcTemplate.update(
"DELETE FROM reserved WHERE book_id = ?",
availableBook.getIdAsLong());
jdbcTemplate.update(
"DELETE FROM borrowed WHERE book_id = ?",
availableBook.getIdAsLong());
}
@Override
public Optional<AvailableBook> getAvailableBook(Long bookId) {
try {
return Optional.ofNullable(
jdbcTemplate.queryForObject(
"SELECT book_id FROM available WHERE book_id = ?",
AvailableBook.class,
bookId));
} catch (DataAccessException exception) {
return Optional.empty();
}
}
@Override
public Optional<ActiveUser> getActiveUser(Long userId) {
try {
jdbcTemplate.queryForObject(
"SELECT id FROM public.library_user as u WHERE u.id = ?",
Long.class,
userId);
} catch (DataAccessException exception) {
return Optional.empty();
}
List<ReservedBook> reservedBooksByUser = getReservedBooksByUser(userId);
List<BorrowedBook> borrowedBooksByUser = getBorrowedBooksByUser(userId);
return Optional.of(new ActiveUser(userId, reservedBooksByUser, borrowedBooksByUser));
}
@Override
public ReservationDetails save(ReservedBook reservedBook) {
jdbcTemplate.update(
"INSERT INTO reserved (book_id, user_id, reserved_date) VALUES (?, ?, ?)",
reservedBook.getIdAsLong(),
reservedBook.getAssignedUserIdAsLong(),
Timestamp.from(reservedBook.getReservedDateAsInstant()));
jdbcTemplate.update(
"DELETE FROM available WHERE book_id = ?",
reservedBook.getIdAsLong());
ReservationId reservationId = jdbcTemplate.queryForObject(
"SELECT id FROM reserved WHERE book_id = ?",
ReservationId.class,
reservedBook.getIdAsLong());
return new ReservationDetails(reservationId, reservedBook);
}
@Override
public void save(BorrowedBook borrowedBook) {
jdbcTemplate.update(
"INSERT INTO borrowed (book_id, user_id, borrowed_date) VALUES (?, ?, ?)",
borrowedBook.getIdAsLong(),
borrowedBook.getAssignedUserIdAsLong(),
Timestamp.from(borrowedBook.getBorrowedDateAsInstant()));
jdbcTemplate.update(
"DELETE FROM reserved WHERE book_id = ?",
borrowedBook.getIdAsLong());
jdbcTemplate.update(
"DELETE FROM available WHERE book_id = ?",
borrowedBook.getIdAsLong());
}
@Override
public List<OverdueReservation> findReservationsForMoreThan(Long days) {
List<OverdueReservationEntity> entities = jdbcTemplate.query(
"SELECT id AS reservationId, book_id AS bookIdentification FROM reserved WHERE DATEADD(day, ?, reserved_date) > NOW()",
new BeanPropertyRowMapper<OverdueReservationEntity>(OverdueReservationEntity.class),
days);
return entities.stream()
.map(entity -> new OverdueReservation(entity.getReservationId(), entity.getBookIdentification()))
.collect(Collectors.toList());
}
@Override
public Optional<ReservedBook> getReservedBook(Long bookId) {
try {
return Optional.ofNullable(
jdbcTemplate.queryForObject(
"SELECT book_id, user_id, reserved_date FROM reserved WHERE book_id = ?",
new ReservedBookRowMapper(),
bookId));
} catch (DataAccessException exception) {
return Optional.empty();
}
}
@Override
public Optional<BorrowedBook> getBorrowedBook(Long bookId) {
try {
return Optional.ofNullable(
jdbcTemplate.queryForObject(
"SELECT book_id, user_id, borrowed_date FROM borrowed WHERE book_id = ?",
new BorrowedBookRowMapper(),
bookId));
} catch (DataAccessException exception) {
return Optional.empty();
}
}
private List<ReservedBook> getReservedBooksByUser(Long userId) {
try {
return jdbcTemplate.query(
"SELECT book_id, user_id, reserved_date FROM reserved WHERE user_id = ?",
new ReservedBookRowMapper(),
userId
);
} catch (DataAccessException exception){
return new ArrayList<>();
}
}
private List<BorrowedBook> getBorrowedBooksByUser(Long userId) {
try {
return jdbcTemplate.query(
"SELECT book_id, user_id, borrowed_date FROM borrowed WHERE user_id = ?",
new BorrowedBookRowMapper(),
userId
);
} catch (DataAccessException exception){
return new ArrayList<>();
}
}
}

View File

@@ -0,0 +1,19 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservedEvent;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.ports.outgoing.BorrowingEventPublisher;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class SpringBorrowingEventPublisherAdapter implements BorrowingEventPublisher {
private final ApplicationEventPublisher eventPublisher;
@Override
public void publish(BookReservedEvent event) {
eventPublisher.publishEvent(event);
}
}

View File

@@ -0,0 +1,9 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure.entity;
import lombok.Data;
@Data
public class OverdueReservationEntity {
private Long reservationId;
private Long bookIdentification;
}

View File

@@ -0,0 +1,20 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure.mapper;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BorrowedBook;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
public class BorrowedBookRowMapper implements RowMapper<BorrowedBook> {
@Override
public BorrowedBook mapRow(ResultSet rs, int rowNum) throws SQLException {
return new BorrowedBook(
rs.getLong("book_id"),
rs.getLong("user_id"),
rs.getTimestamp("borrowed_date").toInstant()
);
}
}

View File

@@ -0,0 +1,19 @@
package io.wkrzywiec.hexagonal.library.domain.borrowing.infrastructure.mapper;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.ReservedBook;
import org.springframework.jdbc.core.RowMapper;
import java.sql.ResultSet;
import java.sql.SQLException;
public class ReservedBookRowMapper implements RowMapper<ReservedBook> {
@Override
public ReservedBook mapRow(ResultSet rs, int rowNum) throws SQLException {
return new ReservedBook(
rs.getLong("book_id"),
rs.getLong("user_id"),
rs.getTimestamp("reserved_date").toInstant()
);
}
}

View File

@@ -0,0 +1,24 @@
package io.wkrzywiec.hexagonal.library.domain.email.application;
import io.wkrzywiec.hexagonal.library.domain.borrowing.core.model.BookReservedEvent;
import io.wkrzywiec.hexagonal.library.domain.email.core.model.SendReservationConfirmationCommand;
import io.wkrzywiec.hexagonal.library.domain.email.core.ports.incoming.SendReservationConfirmation;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class BookReservedEventHandler {
private final SendReservationConfirmation sendReservationConfirmation;
@EventListener
public void handle(BookReservedEvent event) {
sendReservationConfirmation.handle(
new SendReservationConfirmationCommand(
event.getReservationIdAsLong(),
event.getUserIdAsLong(),
event.getBookIdAsLong()));
}
}

View File

@@ -0,0 +1,18 @@
package io.wkrzywiec.hexagonal.library.domain.email.core;
import io.wkrzywiec.hexagonal.library.domain.email.core.model.EmailAddress;
import io.wkrzywiec.hexagonal.library.domain.email.core.model.ReservationConfirmEmail;
class EmailCreator {
static ReservationConfirmEmail reservationEmail(Long reservationId, String bookTitle, String emailTo){
EmailAddress from = new EmailAddress("tom@library.com");
EmailAddress to = new EmailAddress(emailTo);
String subject = String.format("Library - book reservation confirmation (id - %d)", reservationId);
String content = String.format("Dear reader,%n you have reserved a %s book which will be waiting for you in our library for next 2 days. Your reservation id is %d. %n Have a nice day, %n Library",
bookTitle, reservationId);
return new ReservationConfirmEmail(from, to, subject, content);
}
}

View File

@@ -0,0 +1,32 @@
package io.wkrzywiec.hexagonal.library.domain.email.core;
import io.wkrzywiec.hexagonal.library.domain.email.core.model.ReservationConfirmEmail;
import io.wkrzywiec.hexagonal.library.domain.email.core.model.SendReservationConfirmationCommand;
import io.wkrzywiec.hexagonal.library.domain.email.core.ports.incoming.SendReservationConfirmation;
import io.wkrzywiec.hexagonal.library.domain.email.core.ports.outgoing.EmailSender;
import io.wkrzywiec.hexagonal.library.domain.email.core.ports.outgoing.EmailDatabase;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class EmailFacade implements SendReservationConfirmation {
private final EmailSender emailSender;
private final EmailDatabase database;
@Override
public void handle(SendReservationConfirmationCommand sendReservationConfirmation) {
String bookTitle = database
.getTitleByBookId(sendReservationConfirmation.getBookId())
.orElseThrow(() -> new IllegalArgumentException("Can't get book title from database. Reason: there is no book with an id: " + sendReservationConfirmation.getBookId()));
String userEmailAddress = database
.getUserEmailAddress(sendReservationConfirmation.getUserId())
.orElseThrow(() -> new IllegalArgumentException("Can't get email address from database. Reason: there is no user with an id: " + sendReservationConfirmation.getUserId()));
ReservationConfirmEmail reservationConfirmEmail = EmailCreator.reservationEmail(
sendReservationConfirmation.getReservationId(),
bookTitle,
userEmailAddress
);
emailSender.sendReservationConfirmationEmail(reservationConfirmEmail);
}
}

View File

@@ -0,0 +1,24 @@
package io.wkrzywiec.hexagonal.library.domain.email.core.model;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class EmailAddress {
private final String value;
public EmailAddress(String value) {
Pattern pattern = Pattern.compile("^(.+)@(.+)$");
Matcher matcher = pattern.matcher(value);
if(matcher.matches()){
this.value = value;
} else {
throw new IllegalArgumentException("Provided value is not an email address");
}
}
public String getAsString() {
return value;
}
}

View File

@@ -0,0 +1,28 @@
package io.wkrzywiec.hexagonal.library.domain.email.core.model;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class ReservationConfirmEmail {
private final EmailAddress from;
private final EmailAddress to;
private final String subject;
private final String content;
public String getFromEmailAddressAsString(){
return this.from.getAsString();
}
public String getToEmailAddressAsString(){
return this.to.getAsString();
}
public String getSubjectAsString(){
return this.subject;
}
public String getContentAsString(){
return this.content;
}
}

View File

@@ -0,0 +1,17 @@
package io.wkrzywiec.hexagonal.library.domain.email.core.model;
import lombok.Getter;
@Getter
public class SendReservationConfirmationCommand {
private final Long reservationId;
private final Long userId;
private final Long bookId;
public SendReservationConfirmationCommand(Long reservationId, Long userId, Long bookId) {
this.reservationId = reservationId;
this.userId = userId;
this.bookId = bookId;
}
}

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.email.core.ports.incoming;
import io.wkrzywiec.hexagonal.library.domain.email.core.model.SendReservationConfirmationCommand;
public interface SendReservationConfirmation {
void handle(SendReservationConfirmationCommand reservationConfirmationCommand);
}

View File

@@ -0,0 +1,8 @@
package io.wkrzywiec.hexagonal.library.domain.email.core.ports.outgoing;
import java.util.Optional;
public interface EmailDatabase {
Optional<String> getTitleByBookId(Long bookId);
Optional<String> getUserEmailAddress(Long userId);
}

View File

@@ -0,0 +1,8 @@
package io.wkrzywiec.hexagonal.library.domain.email.core.ports.outgoing;
import io.wkrzywiec.hexagonal.library.domain.email.core.model.ReservationConfirmEmail;
public interface EmailSender {
void sendReservationConfirmationEmail(ReservationConfirmEmail reservationConfirmEmail);
}

View File

@@ -0,0 +1,38 @@
package io.wkrzywiec.hexagonal.library.domain.email.infrastructure;
import io.wkrzywiec.hexagonal.library.domain.email.core.ports.outgoing.EmailDatabase;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.Optional;
@RequiredArgsConstructor
public class EmailDatabaseAdapter implements EmailDatabase {
private final JdbcTemplate jdbcTemplate;
@Override
public Optional<String> getTitleByBookId(Long bookId) {
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(
"SELECT title FROM book WHERE id = ?",
String.class,
bookId));
} catch (DataAccessException ex){
return Optional.empty();
}
}
@Override
public Optional<String> getUserEmailAddress(Long userId) {
try {
return Optional.ofNullable(jdbcTemplate.queryForObject(
"SELECT email FROM library_user WHERE id = ?",
String.class,
userId));
} catch (DataAccessException ex){
return Optional.empty();
}
}
}

View File

@@ -0,0 +1,38 @@
package io.wkrzywiec.hexagonal.library.domain.email.infrastructure;
import com.sendgrid.Method;
import com.sendgrid.Request;
import com.sendgrid.SendGrid;
import com.sendgrid.helpers.mail.Mail;
import com.sendgrid.helpers.mail.objects.Content;
import com.sendgrid.helpers.mail.objects.Email;
import io.wkrzywiec.hexagonal.library.domain.email.core.model.ReservationConfirmEmail;
import io.wkrzywiec.hexagonal.library.domain.email.core.ports.outgoing.EmailSender;
import java.io.IOException;
public class SendGridEmailSender implements EmailSender {
@Override
public void sendReservationConfirmationEmail(ReservationConfirmEmail reservationConfirmEmail) {
Email from = new Email(reservationConfirmEmail.getFromEmailAddressAsString());
Email to = new Email(reservationConfirmEmail.getToEmailAddressAsString());
Content content = new Content("text/plain", reservationConfirmEmail.getContentAsString());
Mail mail = new Mail(
from,
reservationConfirmEmail.getSubjectAsString(),
to,
content);
SendGrid sg = new SendGrid(System.getenv("SENDGRID_API_KEY"));
Request request = new Request();
try {
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());
sg.api(request);
} catch (IOException ex) {
System.out.print(ex);
}
}
}

View File

@@ -1,7 +1,7 @@
package io.wkrzywiec.hexagonal.library.inventory.application;
package io.wkrzywiec.hexagonal.library.domain.inventory.application;
import io.wkrzywiec.hexagonal.library.inventory.model.AddNewBookCommand;
import io.wkrzywiec.hexagonal.library.inventory.ports.incoming.AddNewBook;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.AddNewBookCommand;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.incoming.AddNewBook;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

View File

@@ -1,12 +1,12 @@
package io.wkrzywiec.hexagonal.library.inventory;
package io.wkrzywiec.hexagonal.library.domain.inventory.core;
import io.wkrzywiec.hexagonal.library.inventory.model.AddNewBookCommand;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
import io.wkrzywiec.hexagonal.library.inventory.ports.incoming.AddNewBook;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryEventPublisher;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.GetBookDetails;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryDatabase;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.AddNewBookCommand;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.Book;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.NewBookWasAddedEvent;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.incoming.AddNewBook;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.outgoing.InventoryEventPublisher;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.outgoing.GetBookDetails;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.outgoing.InventoryDatabase;
public class InventoryFacade implements AddNewBook{

View File

@@ -1,8 +1,7 @@
package io.wkrzywiec.hexagonal.library.inventory.model;
package io.wkrzywiec.hexagonal.library.domain.inventory.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;

View File

@@ -1,4 +1,4 @@
package io.wkrzywiec.hexagonal.library.inventory.model;
package io.wkrzywiec.hexagonal.library.domain.inventory.core.model;
import lombok.EqualsAndHashCode;
import lombok.ToString;

View File

@@ -1,4 +1,4 @@
package io.wkrzywiec.hexagonal.library.inventory.model;
package io.wkrzywiec.hexagonal.library.domain.inventory.core.model;
import lombok.EqualsAndHashCode;

View File

@@ -1,4 +1,4 @@
package io.wkrzywiec.hexagonal.library.inventory.model;
package io.wkrzywiec.hexagonal.library.domain.inventory.core.model;
import lombok.EqualsAndHashCode;

View File

@@ -1,4 +1,4 @@
package io.wkrzywiec.hexagonal.library.inventory.model;
package io.wkrzywiec.hexagonal.library.domain.inventory.core.model;
import lombok.EqualsAndHashCode;

View File

@@ -1,7 +1,6 @@
package io.wkrzywiec.hexagonal.library.inventory.model;
package io.wkrzywiec.hexagonal.library.domain.inventory.core.model;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.persistence.Column;
import javax.persistence.Embeddable;

View File

@@ -1,4 +1,4 @@
package io.wkrzywiec.hexagonal.library.inventory.model;
package io.wkrzywiec.hexagonal.library.domain.inventory.core.model;
import java.time.Instant;

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.incoming;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.AddNewBookCommand;
public interface AddNewBook {
void handle(AddNewBookCommand addNewBookCommand);
}

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.outgoing;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.Book;
public interface GetBookDetails {
Book handle(String bookId);
}

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.outgoing;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.Book;
public interface InventoryDatabase {
Book save(Book book);
}

View File

@@ -0,0 +1,7 @@
package io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.outgoing;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.NewBookWasAddedEvent;
public interface InventoryEventPublisher {
void publishNewBookWasAddedEvent(NewBookWasAddedEvent event);
}

View File

@@ -1,6 +1,6 @@
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
package io.wkrzywiec.hexagonal.library.domain.inventory.infrastructure;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.Book;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;

View File

@@ -1,14 +1,14 @@
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
package io.wkrzywiec.hexagonal.library.domain.inventory.infrastructure;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import io.wkrzywiec.hexagonal.library.inventory.model.Author;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import io.wkrzywiec.hexagonal.library.inventory.model.BookIdentification;
import io.wkrzywiec.hexagonal.library.inventory.model.Isbn10;
import io.wkrzywiec.hexagonal.library.inventory.model.Isbn13;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.GetBookDetails;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.Author;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.Book;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.BookIdentification;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.Isbn10;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.Isbn13;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.outgoing.GetBookDetails;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;

View File

@@ -1,7 +1,7 @@
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
package io.wkrzywiec.hexagonal.library.domain.inventory.infrastructure;
import io.wkrzywiec.hexagonal.library.inventory.model.Book;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryDatabase;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.Book;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.outgoing.InventoryDatabase;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor

View File

@@ -1,7 +1,7 @@
package io.wkrzywiec.hexagonal.library.inventory.infrastructure;
package io.wkrzywiec.hexagonal.library.domain.inventory.infrastructure;
import io.wkrzywiec.hexagonal.library.inventory.model.NewBookWasAddedEvent;
import io.wkrzywiec.hexagonal.library.inventory.ports.outgoing.InventoryEventPublisher;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.model.NewBookWasAddedEvent;
import io.wkrzywiec.hexagonal.library.domain.inventory.core.ports.outgoing.InventoryEventPublisher;
import lombok.RequiredArgsConstructor;
import org.springframework.context.ApplicationEventPublisher;

View File

@@ -0,0 +1,25 @@
package io.wkrzywiec.hexagonal.library.domain.user.application;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.AddUserCommand;
import io.wkrzywiec.hexagonal.library.domain.user.core.ports.incoming.AddNewUser;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserCommandController {
private final AddNewUser addNewUser;
@PostMapping("")
public ResponseEntity<String> addNewUser(@RequestBody AddUserCommand addUserCommand){
addNewUser.handle(addUserCommand);
return new ResponseEntity<>("New user was added to library", HttpStatus.CREATED);
}
}

View File

@@ -0,0 +1,25 @@
package io.wkrzywiec.hexagonal.library.domain.user.core;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.AddUserCommand;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.EmailAddress;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.User;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.UserIdentifier;
import io.wkrzywiec.hexagonal.library.domain.user.core.ports.incoming.AddNewUser;
import io.wkrzywiec.hexagonal.library.domain.user.core.ports.outgoing.UserDatabase;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public class UserFacade implements AddNewUser {
private final UserDatabase database;
@Override
public UserIdentifier handle(AddUserCommand addUserCommand) {
User user = new User(
new EmailAddress(addUserCommand.getEmail()),
addUserCommand.getFirstName(),
addUserCommand.getLastName()
);
return database.save(user);
}
}

View File

@@ -0,0 +1,17 @@
package io.wkrzywiec.hexagonal.library.domain.user.core.model;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class AddUserCommand {
private String email;
private String firstName;
private String lastName;
}

View File

@@ -0,0 +1,28 @@
package io.wkrzywiec.hexagonal.library.domain.user.core.model;
import lombok.EqualsAndHashCode;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Embeddable
@EqualsAndHashCode
public class EmailAddress {
@Column(name="email")
private String value;
public EmailAddress(String value) {
Pattern pattern = Pattern.compile("^(.+)@(.+)$");
Matcher matcher = pattern.matcher(value);
if(matcher.matches()){
this.value = value;
} else {
throw new IllegalArgumentException("Provided value is not an email address");
}
}
private EmailAddress(){}
}

View File

@@ -0,0 +1,43 @@
package io.wkrzywiec.hexagonal.library.domain.user.core.model;
import lombok.EqualsAndHashCode;
import javax.persistence.Column;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity
@Table(name="library_user")
@EqualsAndHashCode
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@EqualsAndHashCode.Exclude
private Long id;
@Embedded
private EmailAddress emailAddress;
@Column(name="first_name")
private String firstName;
@Column(name="last_name")
private String lastName;
public User(EmailAddress emailAddress, String firstName, String lastName) {
this.emailAddress = emailAddress;
this.firstName = firstName;
this.lastName = lastName;
}
public Long getIdentifierAsLong(){
return id;
}
private User(){}
}

View File

@@ -0,0 +1,15 @@
package io.wkrzywiec.hexagonal.library.domain.user.core.model;
public class UserIdentifier {
private final Long id;
public UserIdentifier(Long id) {
this.id = id;
}
public Long getAsLong(){
return id;
}
}

View File

@@ -0,0 +1,8 @@
package io.wkrzywiec.hexagonal.library.domain.user.core.ports.incoming;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.AddUserCommand;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.UserIdentifier;
public interface AddNewUser {
UserIdentifier handle(AddUserCommand addUserCommand);
}

View File

@@ -0,0 +1,8 @@
package io.wkrzywiec.hexagonal.library.domain.user.core.ports.outgoing;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.User;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.UserIdentifier;
public interface UserDatabase {
UserIdentifier save(User user);
}

View File

@@ -0,0 +1,18 @@
package io.wkrzywiec.hexagonal.library.domain.user.infrastructure;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.User;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.UserIdentifier;
import io.wkrzywiec.hexagonal.library.domain.user.core.ports.outgoing.UserDatabase;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class UserDatabaseAdapter implements UserDatabase {
private final UserRepository userRepository;
@Override
public UserIdentifier save(User user) {
User savedUser = userRepository.save(user);
return new UserIdentifier(savedUser.getIdentifierAsLong());
}
}

View File

@@ -0,0 +1,9 @@
package io.wkrzywiec.hexagonal.library.domain.user.infrastructure;
import io.wkrzywiec.hexagonal.library.domain.user.core.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserRepository extends CrudRepository<User, Long> {
}

Some files were not shown because too many files have changed in this diff Show More