├── orders-common ├── build.gradle └── src │ └── main │ └── java │ └── net │ └── chrisrichardson │ └── eventstore │ └── examples │ └── customersandorders │ └── orderscommmon │ ├── CreateOrderResponse.java │ └── CreateOrderRequest.java ├── customers-common ├── build.gradle └── src │ └── main │ └── java │ └── net │ └── chrisrichardson │ └── eventstore │ └── examples │ └── customersandorders │ └── customerscommon │ ├── CreateCustomerResponse.java │ ├── CreateCustomerRequest.java │ └── GetCustomerResponse.java ├── orders-history-common ├── build.gradle └── src │ └── main │ └── java │ └── net │ └── chrisrichardson │ └── eventstore │ └── examples │ └── customersandorders │ └── ordershistorycommon │ ├── OrderView.java │ ├── CustomerView.java │ └── OrderInfo.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── setenv-circle-ci.sh ├── orders-history-view-service ├── src │ ├── test │ │ ├── resources │ │ │ └── application.yml │ │ └── java │ │ │ └── net │ │ │ └── chrisrichardson │ │ │ └── eventstore │ │ │ └── examples │ │ │ └── customersandorders │ │ │ └── views │ │ │ └── orderhistory │ │ │ ├── CustomerViewRepositoryIntegrationTest.java │ │ │ └── OrderHistoryViewServiceTest.java │ └── main │ │ ├── resources │ │ └── application.yml │ │ └── java │ │ └── net │ │ └── chrisrichardson │ │ └── eventstore │ │ └── examples │ │ └── customersandorders │ │ └── ordershistoryviewservice │ │ ├── OrderHistoryQuerySideService.java │ │ ├── backend │ │ ├── OrderHistoryViewBackendFactory.java │ │ ├── OrderHistoryViewService.java │ │ ├── OrderViewRepository.java │ │ ├── AbstractRepository.java │ │ ├── OrderHistoryViewWorkflow.java │ │ └── CustomerViewRepository.java │ │ └── web │ │ ├── orders │ │ └── OrderViewController.java │ │ └── customers │ │ └── CustomerOrderHistoryController.java ├── docker-compose-integration-tests.yml ├── Dockerfile └── build.gradle ├── orders-service ├── Dockerfile ├── src │ ├── main │ │ ├── java │ │ │ └── net │ │ │ │ └── chrisrichardson │ │ │ │ └── eventstore │ │ │ │ └── examples │ │ │ │ └── customersandorders │ │ │ │ └── ordersservice │ │ │ │ ├── backend │ │ │ │ ├── Customer.java │ │ │ │ ├── ApproveOrderCommand.java │ │ │ │ ├── RejectOrderCommand.java │ │ │ │ ├── CustomerNotFoundException.java │ │ │ │ ├── OrderCommand.java │ │ │ │ ├── CustomerService.java │ │ │ │ ├── OrderService.java │ │ │ │ ├── CustomerServiceProxyFactory.java │ │ │ │ ├── CreateOrderCommand.java │ │ │ │ ├── OrderBackendFactory.java │ │ │ │ ├── OrderServiceImpl.java │ │ │ │ ├── OrderWorkflow.java │ │ │ │ ├── Order.java │ │ │ │ └── CustomerServiceProxy.java │ │ │ │ ├── OrdersServiceMain.java │ │ │ │ └── web │ │ │ │ └── OrderController.java │ │ └── resources │ │ │ └── application.yml │ └── test │ │ ├── resources │ │ └── application.yml │ │ └── java │ │ └── net │ │ └── chrisrichardson │ │ └── eventstore │ │ └── examples │ │ └── customersandorders │ │ └── ordersservice │ │ └── backend │ │ ├── TestData.java │ │ ├── CustomerServiceProxyTest.java │ │ ├── CustomerServiceProxyIntegrationTest.java │ │ └── OrderTest.java └── build.gradle ├── customers-service ├── Dockerfile ├── src │ ├── test │ │ ├── resources │ │ │ └── application.yml │ │ └── java │ │ │ └── net │ │ │ └── chrisrichardson │ │ │ └── eventstore │ │ │ └── examples │ │ │ └── customersandorders │ │ │ └── customersservice │ │ │ └── backend │ │ │ ├── CustomerMother.java │ │ │ ├── CustomerPersistenceTest.java │ │ │ ├── CustomerServiceInProcessComponentTest.java │ │ │ ├── CustomerPropertyTest.java │ │ │ ├── CustomerServiceTest.java │ │ │ └── CustomerTest.java │ └── main │ │ ├── java │ │ └── net │ │ │ └── chrisrichardson │ │ │ └── eventstore │ │ │ └── examples │ │ │ └── customersandorders │ │ │ └── customersservice │ │ │ ├── backend │ │ │ ├── CustomerCommand.java │ │ │ ├── CustomerService.java │ │ │ ├── ReservedCreditTracker.java │ │ │ ├── CreateCustomerCommand.java │ │ │ ├── ReserveCreditCommand.java │ │ │ ├── CustomerBackendFactory.java │ │ │ ├── CustomerWorkflow.java │ │ │ ├── CustomerServiceImpl.java │ │ │ └── Customer.java │ │ │ ├── CustomersServiceMain.java │ │ │ └── web │ │ │ └── CustomerController.java │ │ └── resources │ │ └── application.yml └── build.gradle ├── mongodb-cli.sh ├── mysql-cli.sh ├── common ├── src │ ├── main │ │ ├── java │ │ │ └── net │ │ │ │ └── chrisrichardson │ │ │ │ └── eventstore │ │ │ │ └── examples │ │ │ │ └── customersandorders │ │ │ │ └── common │ │ │ │ ├── order │ │ │ │ ├── OrderState.java │ │ │ │ ├── OrderEvent.java │ │ │ │ ├── OrderApprovedEvent.java │ │ │ │ ├── OrderRejectedEvent.java │ │ │ │ └── OrderCreatedEvent.java │ │ │ │ ├── customer │ │ │ │ ├── CustomerEvent.java │ │ │ │ ├── CustomerCreditLimitExceededEvent.java │ │ │ │ ├── CustomerCreatedEvent.java │ │ │ │ └── CustomerCreditReservedEvent.java │ │ │ │ └── domain │ │ │ │ └── Money.java │ │ └── resources │ │ │ └── logback.xml │ └── test │ │ └── java │ │ └── net │ │ └── chrisrichardson │ │ └── eventstore │ │ └── examples │ │ └── customersandorders │ │ └── common │ │ └── domain │ │ ├── MoneyTest.java │ │ └── MoneyPropertyTest.java └── build.gradle ├── common-test ├── build.gradle └── src │ └── main │ ├── resources │ └── logback.xml │ └── java │ └── net │ └── chrisrichardson │ └── eventstore │ └── examples │ └── customersandorders │ └── commontest │ └── AbstractCustomerAndOrdersIntegrationTest.java ├── .gitignore ├── settings.gradle ├── .circleci ├── upgrade-docker-compose.sh ├── generate-config.sh └── config.yml ├── README.md ├── run.sh ├── show-urls.sh ├── wait-for-services.sh ├── set-env-eventuate-local-mysql.sh ├── set-env.sh ├── set-env-eventuate-local-postgres-wal.sh ├── set-env-eventuate-local-postgres-polling.sh ├── e2e-test ├── build.gradle └── src │ └── test │ └── java │ └── net │ └── chrisrichardson │ └── eventstore │ └── examples │ └── customersandorders │ └── e2etests │ ├── CustomersAndOrdersE2ETestConfiguration.java │ └── CustomersAndOrdersE2ETest.java ├── gradle.properties ├── LICENSE ├── _build-and-test-all.sh ├── gradlew.bat ├── docker-compose-eventuate-local-postgres-wal.yml ├── docker-compose-eventuate-local-postgres-polling.yml ├── docker-compose-eventuate-local-mysql.yml └── gradlew /orders-common/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(":common") 3 | 4 | testImplementation "junit:junit:4.12" 5 | } 6 | -------------------------------------------------------------------------------- /customers-common/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(":common") 3 | 4 | testImplementation "junit:junit:4.12" 5 | } 6 | -------------------------------------------------------------------------------- /orders-history-common/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(":common") 3 | testImplementation "junit:junit:4.12" 4 | } 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eventuate-examples/eventuate-micronaut-examples-customers-and-orders/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /setenv-circle-ci.sh: -------------------------------------------------------------------------------- 1 | 2 | # Host DNS name doesn't resolve in Docker alpine images 3 | 4 | export DOCKER_HOST_IP=$(hostname -I | sed -e 's/ .*//g') 5 | export TERM=dumb 6 | -------------------------------------------------------------------------------- /orders-history-view-service/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | mongodb: 2 | uri: ${SPRING_DATA_MONGODB_URI} 3 | 4 | eventuatelocal.kafka.bootstrap.servers: localhost:9092 5 | -------------------------------------------------------------------------------- /orders-history-view-service/docker-compose-integration-tests.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mongodb: 3 | image: mongo:4.2.12 4 | hostname: mongodb 5 | ports: 6 | - "27017:27017" 7 | -------------------------------------------------------------------------------- /orders-service/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG baseImageVersion 2 | FROM eventuateio/eventuate-examples-docker-images-micronaut-example-base-image:$baseImageVersion 3 | COPY build/libs/orders-service.jar service.jar 4 | -------------------------------------------------------------------------------- /customers-service/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG baseImageVersion 2 | FROM eventuateio/eventuate-examples-docker-images-micronaut-example-base-image:$baseImageVersion 3 | COPY build/libs/customers-service.jar service.jar 4 | -------------------------------------------------------------------------------- /mongodb-cli.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | docker run --network eventuateexamplesjavacustomersandorders_default --link eventuateexamplesjavacustomersandorders_mongodb_1:mongodb -i -t mongo:3.6 /usr/bin/mongo --host mongodb 4 | -------------------------------------------------------------------------------- /orders-history-view-service/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG baseImageVersion 2 | FROM eventuateio/eventuate-examples-docker-images-micronaut-example-base-image:$baseImageVersion 3 | COPY build/libs/orders-history-view-service.jar service.jar 4 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/Customer.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | public class Customer { 4 | } 5 | -------------------------------------------------------------------------------- /customers-service/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | datasources: 2 | default: 3 | url: jdbc:h2:mem:testdb 4 | username: sa 5 | password: test 6 | driverClassName: org.h2.Driver 7 | 8 | eventuatelocal.kafka.bootstrap.servers: localhost:9092 9 | -------------------------------------------------------------------------------- /mysql-cli.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | docker run $* \ 4 | --network eventuateexamplesjavacustomersandorders_default \ 5 | --name mysqlterm --rm mysql/mysql-server:8.0.27-1.2.6-server \ 6 | sh -c 'exec mysql -hmysql -P3306 -uroot -prootpassword eventuate' 7 | -------------------------------------------------------------------------------- /common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/common/order/OrderState.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.order; 2 | 3 | public enum OrderState { 4 | APPROVED, REJECTED, state, CREATED 5 | } 6 | -------------------------------------------------------------------------------- /common-test/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api project(":common") 3 | api project(":orders-history-common") 4 | 5 | api "io.reactivex:rxjava:1.1.5" 6 | api "junit:junit:4.12" 7 | 8 | api 'io.eventuate.util:eventuate-util-test:0.1.0.RELEASE' 9 | 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.ipr 3 | *.iws 4 | .DS_Store 5 | .cache 6 | .classpath 7 | .gradle 8 | .idea 9 | .project 10 | .scala_dependencies 11 | .settings 12 | .springBeans 13 | bin 14 | build 15 | build*.log 16 | classes 17 | node_modules 18 | npm-debug.log 19 | target 20 | out 21 | *.IGNORE -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Jun 28 06:48:46 PDT 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-all.zip 7 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/ApproveOrderCommand.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | public class ApproveOrderCommand implements OrderCommand { 4 | } 5 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/RejectOrderCommand.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | public class RejectOrderCommand implements OrderCommand { 4 | } 5 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include "common" 2 | include "common-test" 3 | include 'orders-history-common' 4 | include 'customers-common' 5 | include 'orders-common' 6 | include 'customers-service' 7 | include 'orders-history-view-service' 8 | include 'orders-service' 9 | 10 | include "e2e-test" 11 | 12 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/CustomerNotFoundException.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | public class CustomerNotFoundException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /.circleci/upgrade-docker-compose.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | docker-compose version 4 | docker version 5 | curl -L https://github.com/docker/compose/releases/download/1.25.4/docker-compose-`uname -s`-`uname -m` > ~/docker-compose 6 | chmod +x ~/docker-compose 7 | sudo mv ~/docker-compose /usr/local/bin/docker-compose 8 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/OrderCommand.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | 4 | import io.eventuate.Command; 5 | 6 | public interface OrderCommand extends Command { 7 | } 8 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/CustomerService.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | public interface CustomerService { 4 | void verifyCustomerCustomerId(String customerId); 5 | } 6 | -------------------------------------------------------------------------------- /customers-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/CustomerCommand.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import io.eventuate.Command; 4 | 5 | public interface CustomerCommand extends Command { 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # An Eventuate project 2 | 3 | 4 | 5 | This project is part of [Eventuate](http://eventuate.io), which is a microservices collaboration platform. 6 | 7 | # eventuate-micronaut-examples-customers-and-orders 8 | Micronaut version of Eventuate event sourcing-based Customers and Orders 9 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | docker="./gradlew mysqlbinlogCompose" 2 | dockercdc="./gradlew mysqlbinlogcdcCompose" 3 | 4 | 5 | 6 | ${docker}Down 7 | 8 | ${dockercdc}Build 9 | ${dockercdc}Up 10 | 11 | ./wait-for-services.sh $DOCKER_HOST_IP "8099" "actuator/health" 12 | 13 | 14 | ${docker}Build 15 | ${docker}Up 16 | 17 | ./wait-for-services.sh $DOCKER_HOST_IP "8081 8082 8083" "health" 18 | -------------------------------------------------------------------------------- /show-urls.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | ./wait-for-services.sh $DOCKER_HOST_IP 8081 8082 8083 4 | 5 | echo The microservices are running 6 | echo You can visit these URLS 7 | echo http://${DOCKER_HOST_IP?}:8081/swagger-ui/index.html - Customer service 8 | echo http://${DOCKER_HOST_IP?}:8083/swagger-ui/index.html - Order Service 9 | echo http://${DOCKER_HOST_IP?}:8082/swagger-ui/index.html - Order History Service 10 | -------------------------------------------------------------------------------- /orders-service/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | customer: 2 | service: 3 | root: 4 | url: http://${DOCKER_HOST_IP}:8081/customers 5 | url: http://${DOCKER_HOST_IP}:8081/customers/{customerId} 6 | datasources: 7 | default: 8 | url: jdbc:h2:mem:testdb 9 | username: sa 10 | password: test 11 | driverClassName: org.h2.Driver 12 | 13 | eventuatelocal.kafka.bootstrap.servers: localhost:9092 14 | -------------------------------------------------------------------------------- /common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/common/order/OrderEvent.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.order; 2 | 3 | import io.eventuate.Event; 4 | import io.eventuate.EventEntity; 5 | 6 | @EventEntity(entity = "net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend.Order") 7 | public interface OrderEvent extends Event { 8 | } 9 | -------------------------------------------------------------------------------- /common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/common/customer/CustomerEvent.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.customer; 2 | 3 | 4 | import io.eventuate.Event; 5 | import io.eventuate.EventEntity; 6 | 7 | @EventEntity(entity = "net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend.Customer") 8 | public interface CustomerEvent extends Event { 9 | } 10 | -------------------------------------------------------------------------------- /orders-history-view-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | endpoints: 2 | health: 3 | enabled: true 4 | sensitive: false 5 | details-visible: ANONYMOUS 6 | mongodb: 7 | uri: ${SPRING_DATA_MONGODB_URI} 8 | micronaut: 9 | router: 10 | static-resources: 11 | swagger-ui: 12 | paths: classpath:META-INF/swagger-ui 13 | mapping: /swagger-ui/** 14 | swagger: 15 | paths: classpath:META-INF/swagger 16 | mapping: /swagger/** -------------------------------------------------------------------------------- /wait-for-services.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | host=$1 4 | ports=$2 5 | path=$3 6 | 7 | shift 3 8 | 9 | done=false 10 | while [[ "$done" = false ]]; do 11 | for port in $ports; do 12 | curl --fail http://${host}:${port}/${path} >& /dev/null 13 | if [[ "$?" -eq "0" ]]; then 14 | done=true 15 | else 16 | done=false 17 | break 18 | fi 19 | done 20 | if [[ "$done" = true ]]; then 21 | echo services are started 22 | break; 23 | fi 24 | echo -n . 25 | sleep 1 26 | done 27 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/OrdersServiceMain.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice; 2 | 3 | import io.micronaut.runtime.Micronaut; 4 | import io.swagger.v3.oas.annotations.OpenAPIDefinition; 5 | 6 | @OpenAPIDefinition 7 | public class OrdersServiceMain { 8 | public static void main(String[] args) { 9 | Micronaut.run(OrdersServiceMain.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /set-env-eventuate-local-mysql.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | . ./set-env.sh 4 | 5 | export SPRING_DATASOURCE_URL=jdbc:mysql://${DOCKER_HOST_IP}/eventuate 6 | export SPRING_DATASOURCE_USERNAME=mysqluser 7 | export SPRING_DATASOURCE_PASSWORD=mysqlpw 8 | export SPRING_DATASOURCE_DRIVER_CLASS_NAME=com.mysql.jdbc.Driver 9 | 10 | export DB_URL=jdbc:mysql://${DOCKER_HOST_IP}/eventuate 11 | export DB_USERNAME=mysqluser 12 | export DB_PASSWORD=mysqlpw 13 | export DB_DRIVERCLASSNAME=com.mysql.jdbc.Driver 14 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/OrderService.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | import io.eventuate.EntityWithIdAndVersion; 4 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 5 | 6 | public interface OrderService { 7 | 8 | EntityWithIdAndVersion createOrder(String customerId, Money orderTotal); 9 | } 10 | -------------------------------------------------------------------------------- /customers-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/CustomersServiceMain.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice; 2 | 3 | import io.micronaut.runtime.Micronaut; 4 | import io.swagger.v3.oas.annotations.OpenAPIDefinition; 5 | 6 | @OpenAPIDefinition 7 | public class CustomersServiceMain { 8 | public static void main(String[] args) { 9 | Micronaut.run(CustomersServiceMain.class); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /orders-common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/orderscommmon/CreateOrderResponse.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.orderscommmon; 2 | 3 | 4 | public class CreateOrderResponse { 5 | private String orderId; 6 | 7 | public CreateOrderResponse() { 8 | } 9 | 10 | public CreateOrderResponse(String orderId) { 11 | this.orderId = orderId; 12 | 13 | } 14 | 15 | public String getOrderId() { 16 | return orderId; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /set-env.sh: -------------------------------------------------------------------------------- 1 | if [ -z "$DOCKER_HOST_IP" ] ; then 2 | if [ -z "$DOCKER_HOST" ] ; then 3 | export DOCKER_HOST_IP=`hostname` 4 | else 5 | echo using ${DOCKER_HOST?} 6 | XX=${DOCKER_HOST%\:*} 7 | export DOCKER_HOST_IP=${XX#tcp\:\/\/} 8 | fi 9 | fi 10 | 11 | echo DOCKER_HOST_IP is $DOCKER_HOST_IP 12 | 13 | export COMPOSE_HTTP_TIMEOUT=240 14 | export SPRING_DATA_MONGODB_URI=mongodb://$DOCKER_HOST_IP/customers_and_orders 15 | export EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS=${DOCKER_HOST_IP}:9092 16 | -------------------------------------------------------------------------------- /.circleci/generate-config.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | cat > generated_config.yml <> generated_config.yml < createCustomer(String name, Money creditLimit); 10 | 11 | EntityWithMetadata findById(String customerId); 12 | } 13 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | setup: true 3 | orbs: 4 | eventuate-gradle-build-and-test: "eventuate_io/eventuate-gradle-build-and-test@0.2.9" 5 | continuation: circleci/continuation@0.1.2 6 | jobs: 7 | setup: 8 | executor: continuation/default 9 | steps: 10 | - checkout # checkout code 11 | - run: # run a command 12 | name: Generate config 13 | command: | 14 | ./.circleci/generate-config.sh > generated_config.yml 15 | - continuation/continue: 16 | configuration_path: generated_config.yml 17 | 18 | workflows: 19 | setup: 20 | jobs: 21 | - setup 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Eventuate, Inc. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /customers-service/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/CustomerMother.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | 5 | public class CustomerMother { 6 | 7 | static Money creditLimit = new Money(56); 8 | static Money orderTotalThatExceedsCreditLimit = creditLimit.add(new Money(1)); 9 | static String name = "Fred"; 10 | static String orderId = "myorder"; 11 | static Money orderTotalWithinCreditLimit = new Money(5); 12 | } 13 | -------------------------------------------------------------------------------- /common-test/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | api 'commons-lang:commons-lang:2.6' 4 | 5 | api "io.eventuate.local.java:eventuate-client-java" 6 | 7 | api "io.eventuate.common:eventuate-common-id:$eventuateCommonVersion" 8 | 9 | testImplementation 'com.pholser:junit-quickcheck-core:0.7' 10 | testImplementation 'com.pholser:junit-quickcheck-generators:0.7' 11 | testImplementation "junit:junit:4.12" 12 | testImplementation "org.springframework.boot:spring-boot-starter-test:$springBootVersion" 13 | testImplementation "io.eventuate.local.java:eventuate-client-java-jdbc" 14 | 15 | api 'jakarta.xml.bind:jakarta.xml.bind-api:2.3.3' 16 | api 'com.sun.xml.bind:jaxb-impl:2.3.3' 17 | } 18 | -------------------------------------------------------------------------------- /common/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /customers-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/ReservedCreditTracker.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class ReservedCreditTracker { 9 | private Map creditReservations = new HashMap<>(); 10 | 11 | Money reservedCredit() { 12 | return creditReservations.values().stream().reduce(Money.ZERO, Money::add); 13 | } 14 | 15 | void addReservation(String orderId, Money orderTotal) { 16 | creditReservations.put(orderId, orderTotal); 17 | } 18 | } -------------------------------------------------------------------------------- /customers-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/CreateCustomerCommand.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | 5 | public class CreateCustomerCommand implements CustomerCommand { 6 | private final String name; 7 | private final Money creditLimit; 8 | 9 | public CreateCustomerCommand(String name, Money creditLimit) { 10 | this.name = name; 11 | this.creditLimit = creditLimit; 12 | } 13 | 14 | public Money getCreditLimit() { 15 | return creditLimit; 16 | } 17 | 18 | public String getName() { 19 | return name; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /customers-common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customerscommon/CreateCustomerRequest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customerscommon; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | 5 | public class CreateCustomerRequest { 6 | private String name; 7 | private Money creditLimit; 8 | 9 | public CreateCustomerRequest() { 10 | } 11 | 12 | public CreateCustomerRequest(String name, Money creditLimit) { 13 | 14 | this.name = name; 15 | this.creditLimit = creditLimit; 16 | } 17 | 18 | 19 | public String getName() { 20 | return name; 21 | } 22 | 23 | public Money getCreditLimit() { 24 | return creditLimit; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /customers-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/ReserveCreditCommand.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | 5 | public class ReserveCreditCommand implements CustomerCommand { 6 | private final Money orderTotal; 7 | private final String orderId; 8 | 9 | public ReserveCreditCommand(Money orderTotal, String orderId) { 10 | this.orderTotal = orderTotal; 11 | this.orderId = orderId; 12 | } 13 | 14 | public Money getOrderTotal() { 15 | return orderTotal; 16 | } 17 | 18 | public String getOrderId() { 19 | return orderId; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /orders-common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/orderscommmon/CreateOrderRequest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.orderscommmon; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | 5 | public class CreateOrderRequest { 6 | private Money orderTotal; 7 | private String customerId; 8 | 9 | public CreateOrderRequest() { 10 | } 11 | 12 | public CreateOrderRequest(String customerId, Money orderTotal) { 13 | this.customerId = customerId; 14 | this.orderTotal = orderTotal; 15 | } 16 | 17 | public Money getOrderTotal() { 18 | return orderTotal; 19 | } 20 | 21 | public String getCustomerId() { 22 | return customerId; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/CreateOrderCommand.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | 5 | public class CreateOrderCommand implements OrderCommand { 6 | private final String customerId; 7 | private final Money orderTotal; 8 | 9 | public CreateOrderCommand(String customerId, Money orderTotal) { 10 | this.customerId = customerId; 11 | this.orderTotal = orderTotal; 12 | } 13 | 14 | public String getCustomerId() { 15 | return customerId; 16 | } 17 | 18 | public Money getOrderTotal() { 19 | return orderTotal; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/OrderBackendFactory.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | import io.eventuate.sync.AggregateRepository; 4 | import io.eventuate.sync.EventuateAggregateStore; 5 | import io.micronaut.context.annotation.Factory; 6 | 7 | import javax.inject.Singleton; 8 | 9 | @Factory 10 | public class OrderBackendFactory { 11 | 12 | @Singleton 13 | public OrderWorkflow orderWorkflow() { 14 | return new OrderWorkflow(); 15 | } 16 | 17 | @Singleton 18 | public AggregateRepository orderRepository(EventuateAggregateStore eventStore) { 19 | return new AggregateRepository<>(Order.class, eventStore); 20 | } 21 | 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /customers-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/CustomerBackendFactory.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import io.eventuate.sync.AggregateRepository; 4 | import io.eventuate.sync.EventuateAggregateStore; 5 | import io.micronaut.context.annotation.Factory; 6 | 7 | import javax.inject.Singleton; 8 | 9 | @Factory 10 | public class CustomerBackendFactory { 11 | 12 | @Singleton 13 | public CustomerWorkflow customerWorkflow() { 14 | return new CustomerWorkflow(); 15 | } 16 | 17 | @Singleton 18 | public AggregateRepository customerRepository(EventuateAggregateStore eventStore) { 19 | return new AggregateRepository<>(Customer.class, eventStore); 20 | } 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/common/order/OrderApprovedEvent.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.order; 2 | 3 | import org.apache.commons.lang.builder.EqualsBuilder; 4 | import org.apache.commons.lang.builder.HashCodeBuilder; 5 | 6 | public class OrderApprovedEvent implements OrderEvent { 7 | 8 | private String customerId; 9 | 10 | private OrderApprovedEvent() { 11 | } 12 | 13 | public OrderApprovedEvent(String customerId) { 14 | this.customerId = customerId; 15 | } 16 | 17 | @Override 18 | public boolean equals(Object obj) { 19 | return EqualsBuilder.reflectionEquals(this, obj); 20 | } 21 | 22 | @Override 23 | public int hashCode() { 24 | return HashCodeBuilder.reflectionHashCode(this); 25 | } 26 | 27 | public String getCustomerId() { 28 | return customerId; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/common/order/OrderRejectedEvent.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.order; 2 | 3 | import org.apache.commons.lang.builder.EqualsBuilder; 4 | import org.apache.commons.lang.builder.HashCodeBuilder; 5 | 6 | public class OrderRejectedEvent implements OrderEvent { 7 | 8 | private String customerId; 9 | 10 | private OrderRejectedEvent() { 11 | } 12 | 13 | public OrderRejectedEvent(String customerId) { 14 | this.customerId = customerId; 15 | } 16 | 17 | @Override 18 | public boolean equals(Object obj) { 19 | return EqualsBuilder.reflectionEquals(this, obj); 20 | } 21 | 22 | @Override 23 | public int hashCode() { 24 | return HashCodeBuilder.reflectionHashCode(this); 25 | } 26 | 27 | public String getCustomerId() { 28 | return customerId; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/OrderServiceImpl.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | import io.eventuate.sync.AggregateRepository; 4 | import io.eventuate.EntityWithIdAndVersion; 5 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 6 | 7 | import javax.inject.Inject; 8 | 9 | public class OrderServiceImpl implements OrderService { 10 | 11 | @Inject 12 | private AggregateRepository orderRepository; 13 | 14 | @Inject 15 | private CustomerService customerService; 16 | 17 | @Override 18 | public EntityWithIdAndVersion createOrder(String customerId, Money orderTotal) { 19 | customerService.verifyCustomerCustomerId(customerId); 20 | return orderRepository.save(new CreateOrderCommand(customerId, orderTotal)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/common/customer/CustomerCreditLimitExceededEvent.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.customer; 2 | 3 | import org.apache.commons.lang.builder.EqualsBuilder; 4 | import org.apache.commons.lang.builder.HashCodeBuilder; 5 | 6 | public class CustomerCreditLimitExceededEvent implements CustomerEvent { 7 | private String orderId; 8 | 9 | @Override 10 | public boolean equals(Object obj) { 11 | return EqualsBuilder.reflectionEquals(this, obj); 12 | } 13 | 14 | @Override 15 | public int hashCode() { 16 | return HashCodeBuilder.reflectionHashCode(this); 17 | } 18 | 19 | public CustomerCreditLimitExceededEvent() { 20 | } 21 | 22 | public CustomerCreditLimitExceededEvent(String orderId) { 23 | this.orderId = orderId; 24 | } 25 | 26 | public String getOrderId() { 27 | return orderId; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /orders-history-view-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordershistoryviewservice/backend/OrderHistoryViewBackendFactory.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend; 2 | 3 | import com.mongodb.client.MongoClient; 4 | import io.micronaut.context.annotation.Factory; 5 | 6 | import javax.inject.Singleton; 7 | 8 | @Factory 9 | public class OrderHistoryViewBackendFactory { 10 | 11 | @Singleton 12 | public OrderHistoryViewWorkflow orderHistoryViewWorkflow(OrderHistoryViewService service) { 13 | return new OrderHistoryViewWorkflow(service); 14 | } 15 | 16 | @Singleton 17 | public CustomerViewRepository customerViewRepository(MongoClient mongoClient) { 18 | return new CustomerViewRepository(mongoClient); 19 | } 20 | 21 | @Singleton 22 | public OrderViewRepository orderViewRepository(MongoClient mongoClient) { 23 | return new OrderViewRepository(mongoClient); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /orders-history-common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordershistorycommon/OrderView.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderState; 5 | 6 | public class OrderView { 7 | 8 | private String id; 9 | 10 | private OrderState state; 11 | private Money orderTotal; 12 | 13 | 14 | public OrderView() { 15 | } 16 | 17 | public OrderView(String id, Money orderTotal) { 18 | this.id = id; 19 | this.orderTotal = orderTotal; 20 | this.state = OrderState.CREATED; 21 | } 22 | 23 | public Money getOrderTotal() { 24 | return orderTotal; 25 | } 26 | 27 | public OrderState getState() { 28 | return state; 29 | } 30 | 31 | public void setState(OrderState state) { 32 | this.state = state; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /orders-history-view-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordershistoryviewservice/web/orders/OrderViewController.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.web.orders; 2 | 3 | import io.micronaut.http.HttpResponse; 4 | import io.micronaut.http.annotation.Controller; 5 | import io.micronaut.http.annotation.Get; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.OrderView; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend.OrderViewRepository; 8 | import javax.inject.Inject; 9 | 10 | @Controller 11 | public class OrderViewController { 12 | 13 | @Inject 14 | private OrderViewRepository orderViewRepository; 15 | 16 | @Get("/orders/{orderId}") 17 | public HttpResponse getOrder(String orderId) { 18 | return orderViewRepository.findById(orderId).map(HttpResponse::ok).orElseGet(HttpResponse::notFound); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /common/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/common/domain/MoneyTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.domain; 2 | 3 | 4 | import org.junit.Test; 5 | 6 | import java.math.BigDecimal; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertTrue; 10 | 11 | public class MoneyTest { 12 | 13 | private Money m1 = new Money(10); 14 | private Money m2 = new Money(15); 15 | 16 | @Test 17 | public void shouldReturnAmount() { 18 | assertEquals(new BigDecimal(10), new Money(10).getAmount()); 19 | } 20 | 21 | @Test 22 | public void shouldCompare() { 23 | assertTrue(m2.isGreaterThanOrEqual(m2)); 24 | assertTrue(m2.isGreaterThanOrEqual(m1)); 25 | } 26 | 27 | @Test 28 | public void shouldAdd() { 29 | assertEquals(new Money(25), m1.add(m2)); 30 | } 31 | 32 | @Test 33 | public void shouldSubtract() { 34 | assertEquals(new Money(5), m2.subtract(m1)); 35 | } 36 | 37 | 38 | 39 | } -------------------------------------------------------------------------------- /orders-history-view-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordershistoryviewservice/web/customers/CustomerOrderHistoryController.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.web.customers; 2 | 3 | import io.micronaut.http.HttpResponse; 4 | import io.micronaut.http.annotation.Controller; 5 | import io.micronaut.http.annotation.Get; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.CustomerView; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend.CustomerViewRepository; 8 | 9 | import javax.inject.Inject; 10 | 11 | @Controller 12 | public class CustomerOrderHistoryController { 13 | 14 | @Inject 15 | private CustomerViewRepository customerViewRepository; 16 | 17 | @Get("/customers/{customerId}") 18 | public HttpResponse getCustomer(String customerId) { 19 | return customerViewRepository.findById(customerId).map(HttpResponse::ok).orElseGet(HttpResponse::notFound); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /orders-history-common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordershistorycommon/CustomerView.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | public class CustomerView { 9 | 10 | private String id; 11 | 12 | 13 | private Map orders = new HashMap<>(); 14 | private String name; 15 | private Money creditLimit; 16 | 17 | public void setId(String id) { 18 | this.id = id; 19 | } 20 | 21 | public String getId() { 22 | return id; 23 | } 24 | 25 | public Map getOrders() { 26 | return orders; 27 | } 28 | 29 | public void setName(String name) { 30 | this.name = name; 31 | } 32 | 33 | public String getName() { 34 | return name; 35 | } 36 | 37 | public void setCreditLimit(Money creditLimit) { 38 | this.creditLimit = creditLimit; 39 | } 40 | 41 | public Money getCreditLimit() { 42 | return creditLimit; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/common/customer/CustomerCreatedEvent.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.customer; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | import org.apache.commons.lang.builder.EqualsBuilder; 5 | import org.apache.commons.lang.builder.HashCodeBuilder; 6 | 7 | public class CustomerCreatedEvent implements CustomerEvent { 8 | private String name; 9 | private Money creditLimit; 10 | 11 | @Override 12 | public boolean equals(Object obj) { 13 | return EqualsBuilder.reflectionEquals(this, obj); 14 | } 15 | 16 | @Override 17 | public int hashCode() { 18 | return HashCodeBuilder.reflectionHashCode(this); 19 | } 20 | 21 | private CustomerCreatedEvent() { 22 | } 23 | 24 | public CustomerCreatedEvent(String name, Money creditLimit) { 25 | 26 | this.name = name; 27 | this.creditLimit = creditLimit; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public Money getCreditLimit() { 35 | return creditLimit; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/common/order/OrderCreatedEvent.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.order; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | import org.apache.commons.lang.builder.EqualsBuilder; 5 | import org.apache.commons.lang.builder.HashCodeBuilder; 6 | 7 | public class OrderCreatedEvent implements OrderEvent { 8 | private Money orderTotal; 9 | private String customerId; 10 | 11 | @Override 12 | public boolean equals(Object obj) { 13 | return EqualsBuilder.reflectionEquals(this, obj); 14 | } 15 | 16 | @Override 17 | public int hashCode() { 18 | return HashCodeBuilder.reflectionHashCode(this); 19 | } 20 | 21 | public OrderCreatedEvent() { 22 | } 23 | 24 | public OrderCreatedEvent(String customerId, Money orderTotal) { 25 | this.customerId = customerId; 26 | this.orderTotal = orderTotal; 27 | } 28 | 29 | public Money getOrderTotal() { 30 | return orderTotal; 31 | } 32 | 33 | public String getCustomerId() { 34 | return customerId; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /orders-history-common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordershistorycommon/OrderInfo.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderState; 5 | 6 | public class OrderInfo { 7 | 8 | private OrderState state; 9 | private String orderId; 10 | private Money orderTotal; 11 | 12 | 13 | public OrderInfo() { 14 | } 15 | 16 | public OrderInfo(String orderId, Money orderTotal) { 17 | this.orderId = orderId; 18 | this.orderTotal = orderTotal; 19 | this.state = OrderState.CREATED; 20 | } 21 | 22 | public void approve() { 23 | state = OrderState.APPROVED; 24 | } 25 | 26 | public void reject() { 27 | state = OrderState.REJECTED; 28 | } 29 | 30 | public Money getOrderTotal() { 31 | return orderTotal; 32 | } 33 | 34 | public OrderState getState() { 35 | return state; 36 | } 37 | 38 | public void setState(OrderState state) { 39 | this.state = state; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/common/customer/CustomerCreditReservedEvent.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.customer; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | import org.apache.commons.lang.builder.EqualsBuilder; 5 | import org.apache.commons.lang.builder.HashCodeBuilder; 6 | 7 | public class CustomerCreditReservedEvent implements CustomerEvent { 8 | private String orderId; 9 | private Money orderTotal; 10 | 11 | @Override 12 | public boolean equals(Object obj) { 13 | return EqualsBuilder.reflectionEquals(this, obj); 14 | } 15 | 16 | @Override 17 | public int hashCode() { 18 | return HashCodeBuilder.reflectionHashCode(this); 19 | } 20 | 21 | public CustomerCreditReservedEvent() { 22 | } 23 | 24 | public CustomerCreditReservedEvent(String orderId, Money orderTotal) { 25 | this.orderId = orderId; 26 | this.orderTotal = orderTotal; 27 | } 28 | 29 | public String getOrderId() { 30 | return orderId; 31 | } 32 | 33 | public Money getOrderTotal() { 34 | return orderTotal; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /customers-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/CustomerWorkflow.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import io.eventuate.*; 4 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 5 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderCreatedEvent; 6 | 7 | import java.util.concurrent.CompletableFuture; 8 | 9 | @EventSubscriber(id = "customerWorkflow") 10 | public class CustomerWorkflow implements Subscriber { 11 | 12 | @EventHandlerMethod 13 | public CompletableFuture> reserveCredit( 14 | EventHandlerContext ctx) { 15 | 16 | System.out.println(String.format("reserveCredit invoked %s", ctx)); 17 | 18 | OrderCreatedEvent event = ctx.getEvent(); 19 | Money orderTotal = event.getOrderTotal(); 20 | String customerId = event.getCustomerId(); 21 | String orderId = ctx.getEntityId(); 22 | 23 | return ctx.update(Customer.class, customerId, new ReserveCreditCommand(orderTotal, orderId)); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /_build-and-test-all.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | set -e 4 | 5 | docker="./gradlew ${database}${mode}Compose" 6 | dockercdc="./gradlew ${database}${mode}cdcCompose" 7 | 8 | 9 | if [ -z "$SPRING_DATA_MONGODB_URI" ] ; then 10 | export SPRING_DATA_MONGODB_URI=mongodb://${DOCKER_HOST_IP?}/customers_orders 11 | echo Set SPRING_DATA_MONGODB_URI $SPRING_DATA_MONGODB_URI 12 | else 13 | echo Already Set SPRING_DATA_MONGODB_URI=$SPRING_DATA_MONGODB_URI 14 | fi 15 | 16 | 17 | if [ "$1" = "--use-existing" ] ; then 18 | shift; 19 | else 20 | ${docker}Down 21 | fi 22 | 23 | NO_RM=false 24 | 25 | if [ "$1" = "--no-rm" ] ; then 26 | NO_RM=true 27 | shift 28 | fi 29 | 30 | ${dockercdc}Build 31 | ${dockercdc}Up 32 | 33 | ./gradlew --stacktrace $BUILD_AND_TEST_ALL_EXTRA_GRADLE_ARGS $* testClasses 34 | ./gradlew --stacktrace $BUILD_AND_TEST_ALL_EXTRA_GRADLE_ARGS $* build -x :e2e-test:test 35 | 36 | ${docker}Build 37 | ${docker}Up 38 | 39 | ./wait-for-services.sh $DOCKER_HOST_IP "8081 8082 8083" "health" 40 | 41 | set -e 42 | 43 | ./gradlew -a $BUILD_AND_TEST_ALL_EXTRA_GRADLE_ARGS $* :e2e-test:cleanTest :e2e-test:test -P ignoreE2EFailures=false 44 | 45 | if [ $NO_RM = false ] ; then 46 | ${docker}Down 47 | fi 48 | -------------------------------------------------------------------------------- /customers-common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customerscommon/GetCustomerResponse.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customerscommon; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | 5 | public class GetCustomerResponse { 6 | private String customerId; 7 | private Money creditLimit; 8 | private Money money; 9 | 10 | public GetCustomerResponse() { 11 | } 12 | 13 | public GetCustomerResponse(String customerId, Money creditLimit, Money money) { 14 | this.customerId = customerId; 15 | this.creditLimit = creditLimit; 16 | this.money = money; 17 | } 18 | 19 | public String getCustomerId() { 20 | return customerId; 21 | } 22 | 23 | public void setCustomerId(String customerId) { 24 | this.customerId = customerId; 25 | } 26 | 27 | public Money getCreditLimit() { 28 | return creditLimit; 29 | } 30 | 31 | public void setCreditLimit(Money creditLimit) { 32 | this.creditLimit = creditLimit; 33 | } 34 | 35 | public Money getMoney() { 36 | return money; 37 | } 38 | 39 | public void setMoney(Money money) { 40 | this.money = money; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /customers-service/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/CustomerPersistenceTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import io.eventuate.EntityWithIdAndVersion; 4 | import io.eventuate.sync.AggregateRepository; 5 | import io.micronaut.test.annotation.MicronautTest; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.inject.Inject; 10 | 11 | @MicronautTest 12 | public class CustomerPersistenceTest { 13 | 14 | @Inject 15 | private AggregateRepository aggregateRepository; 16 | 17 | @Test 18 | public void shouldCreateAndUpdateCustomer() { 19 | EntityWithIdAndVersion cwm = aggregateRepository.save(new CreateCustomerCommand("Fred", new Money(1234))); 20 | 21 | aggregateRepository.update(cwm.getEntityId(), new ReserveCreditCommand(new Money(11), "order-1")); 22 | aggregateRepository.update(cwm.getEntityId(), new ReserveCreditCommand(new Money(11), "order-2")); 23 | aggregateRepository.update(cwm.getEntityId(), new ReserveCreditCommand(new Money(11), "order-3")); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /customers-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/CustomerServiceImpl.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import io.eventuate.sync.AggregateRepository; 4 | import io.eventuate.EntityWithIdAndVersion; 5 | import io.eventuate.EntityWithMetadata; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 7 | 8 | import javax.inject.Inject; 9 | import javax.inject.Singleton; 10 | 11 | @Singleton 12 | public class CustomerServiceImpl implements CustomerService { 13 | 14 | private AggregateRepository customerRepository; 15 | 16 | @Inject 17 | public CustomerServiceImpl(AggregateRepository customerRepository) { 18 | this.customerRepository = customerRepository; 19 | } 20 | 21 | @Override 22 | public EntityWithIdAndVersion createCustomer(String name, Money creditLimit) { 23 | return customerRepository.save(new CreateCustomerCommand(name, creditLimit)); 24 | } 25 | 26 | @Override 27 | public EntityWithMetadata findById(String customerId) { 28 | return customerRepository.find(customerId); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /common/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/common/domain/MoneyPropertyTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.domain; 2 | 3 | import com.pholser.junit.quickcheck.Property; 4 | import com.pholser.junit.quickcheck.generator.InRange; 5 | import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; 6 | import org.junit.runner.RunWith; 7 | 8 | import static org.hamcrest.Matchers.greaterThanOrEqualTo; 9 | import static org.hamcrest.Matchers.lessThanOrEqualTo; 10 | import static org.junit.Assert.assertEquals; 11 | import static org.junit.Assume.assumeThat; 12 | 13 | @RunWith(JUnitQuickcheck.class) 14 | public class MoneyPropertyTest { 15 | 16 | @Property 17 | public void add(int x, @InRange(minInt = 0, maxInt = Integer.MAX_VALUE / 2) int y) { 18 | 19 | assumeThat(x, greaterThanOrEqualTo(0)); 20 | assumeThat(x, lessThanOrEqualTo(Integer.MAX_VALUE / 2)); 21 | 22 | assertEquals(new Money(x).add(new Money(y)), new Money(x + y)); 23 | } 24 | 25 | @Property 26 | public void isGreaterThanOrEqual(int x, int y) { 27 | assertEquals(new Money(x).isGreaterThanOrEqual(new Money(y)), x >= y); 28 | } 29 | 30 | @Property 31 | public void equals(int x, int y) { 32 | assertEquals(new Money(x).equals(new Money(y)), x == y); 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /e2e-test/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/e2etests/CustomersAndOrdersE2ETestConfiguration.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.e2etests; 2 | 3 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 4 | import org.springframework.boot.autoconfigure.web.HttpMessageConverters; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.http.converter.HttpMessageConverter; 8 | import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; 9 | import org.springframework.web.client.RestTemplate; 10 | 11 | import java.util.Arrays; 12 | import java.util.List; 13 | 14 | @Configuration 15 | @EnableAutoConfiguration 16 | public class CustomersAndOrdersE2ETestConfiguration { 17 | 18 | @Bean 19 | public RestTemplate restTemplate(HttpMessageConverters converters) { 20 | RestTemplate restTemplate = new RestTemplate(); 21 | HttpMessageConverter httpMessageConverter = converters.getConverters().get(0); 22 | List> httpMessageConverters = Arrays.asList(new MappingJackson2HttpMessageConverter()); 23 | restTemplate.setMessageConverters((List>) httpMessageConverters); 24 | return restTemplate; 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/OrderWorkflow.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | 4 | import io.eventuate.*; 5 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreditLimitExceededEvent; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreditReservedEvent; 7 | 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | @EventSubscriber(id="orderWorkflow") 11 | public class OrderWorkflow implements Subscriber { 12 | 13 | @EventHandlerMethod 14 | public CompletableFuture> 15 | creditLimitReserved(EventHandlerContext ctx) { 16 | System.out.println(String.format("creditLimitReserved invoked %s", ctx)); 17 | 18 | String orderId = ctx.getEvent().getOrderId(); 19 | 20 | return ctx.update(Order.class, orderId, new ApproveOrderCommand()); 21 | } 22 | 23 | @EventHandlerMethod 24 | public CompletableFuture> 25 | creditLimitExceeded(EventHandlerContext ctx) { 26 | System.out.println(String.format("creditLimitExceeded invoked %s", ctx)); 27 | 28 | String orderId = ctx.getEvent().getOrderId(); 29 | 30 | return ctx.update(Order.class, orderId, new RejectOrderCommand()); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /orders-history-view-service/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/views/orderhistory/CustomerViewRepositoryIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.views.orderhistory; 2 | 3 | import io.micronaut.test.annotation.MicronautTest; 4 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 5 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.CustomerView; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend.CustomerViewRepository; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import javax.inject.Inject; 10 | import java.util.UUID; 11 | 12 | import static org.junit.Assert.assertEquals; 13 | 14 | @MicronautTest 15 | public class CustomerViewRepositoryIntegrationTest { 16 | 17 | @Inject 18 | private CustomerViewRepository customerViewRepository; 19 | 20 | @Test 21 | public void shouldCreateAndFindCustomer() { 22 | String customerId = UUID.randomUUID().toString(); 23 | Money creditLimit = new Money(2000); 24 | String customerName = "Fred"; 25 | 26 | customerViewRepository.addCustomer(customerId, customerName, creditLimit); 27 | CustomerView customerView = customerViewRepository.findById(customerId).get(); 28 | 29 | assertEquals(customerId, customerView.getId()); 30 | assertEquals(customerName, customerView.getName()); 31 | assertEquals(0, customerView.getOrders().size()); 32 | assertEquals(creditLimit, customerView.getCreditLimit()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/web/OrderController.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.web; 2 | 3 | import io.eventuate.EntityWithIdAndVersion; 4 | import io.micronaut.http.HttpResponse; 5 | import io.micronaut.http.annotation.Controller; 6 | import io.micronaut.http.annotation.Post; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.orderscommmon.CreateOrderRequest; 8 | import net.chrisrichardson.eventstore.examples.customersandorders.orderscommmon.CreateOrderResponse; 9 | import net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend.CustomerNotFoundException; 10 | import net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend.Order; 11 | import net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend.OrderService; 12 | 13 | import javax.inject.Inject; 14 | 15 | @Controller 16 | public class OrderController { 17 | 18 | @Inject 19 | private OrderService orderService; 20 | 21 | @Post(value = "/orders") 22 | public HttpResponse createOrder(CreateOrderRequest createOrderRequest) { 23 | try { 24 | EntityWithIdAndVersion order = 25 | orderService.createOrder(createOrderRequest.getCustomerId(), createOrderRequest.getOrderTotal()); 26 | return HttpResponse.ok(new CreateOrderResponse(order.getEntityId())); 27 | } catch (CustomerNotFoundException e) { 28 | return HttpResponse.badRequest(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /orders-history-view-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordershistoryviewservice/backend/OrderHistoryViewService.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderState; 5 | 6 | import javax.inject.Inject; 7 | import javax.inject.Singleton; 8 | 9 | @Singleton 10 | public class OrderHistoryViewService { 11 | 12 | @Inject 13 | private CustomerViewRepository customerViewRepository; 14 | 15 | @Inject 16 | private OrderViewRepository orderViewRepository; 17 | 18 | public void createCustomer(String customerId, String customerName, Money creditLimit) { 19 | customerViewRepository.addCustomer(customerId, customerName, creditLimit); 20 | } 21 | 22 | public void addOrder(String customerId, String orderId, Money orderTotal) { 23 | customerViewRepository.addOrder(customerId, orderId, orderTotal); 24 | orderViewRepository.addOrder(orderId, orderTotal); 25 | } 26 | 27 | public void approveOrder(String customerId, String orderId) { 28 | customerViewRepository.updateOrderState(customerId, orderId, OrderState.APPROVED); 29 | orderViewRepository.updateOrderState(orderId, OrderState.APPROVED); 30 | } 31 | 32 | public void rejectOrder(String customerId, String orderId) { 33 | customerViewRepository.updateOrderState(customerId, orderId, OrderState.REJECTED); 34 | orderViewRepository.updateOrderState(orderId, OrderState.REJECTED); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /common/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/common/domain/Money.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.common.domain; 2 | 3 | import org.apache.commons.lang.builder.EqualsBuilder; 4 | import org.apache.commons.lang.builder.HashCodeBuilder; 5 | import org.apache.commons.lang.builder.ToStringBuilder; 6 | 7 | import java.math.BigDecimal; 8 | 9 | public class Money { 10 | 11 | public static final Money ZERO = new Money(0); 12 | private BigDecimal amount; 13 | 14 | public Money() { 15 | } 16 | 17 | public Money(int i) { 18 | this.amount = new BigDecimal(i); 19 | } 20 | 21 | public Money(BigDecimal amount) { 22 | this.amount = amount; 23 | } 24 | 25 | /* 26 | If this >= other 27 | */ 28 | public boolean isGreaterThanOrEqual(Money other) { 29 | return amount.compareTo(other.amount) >= 0; 30 | } 31 | public Money add(Money other) { 32 | return new Money(amount.add(other.amount)); 33 | } 34 | public Money subtract(Money other) { 35 | return new Money(amount.subtract(other.amount)); 36 | } 37 | 38 | 39 | @Override 40 | public String toString() { 41 | return ToStringBuilder.reflectionToString(this); 42 | } 43 | 44 | @Override 45 | public boolean equals(Object obj) { 46 | return EqualsBuilder.reflectionEquals(this, obj); 47 | } 48 | 49 | @Override 50 | public int hashCode() { 51 | return HashCodeBuilder.reflectionHashCode(this); 52 | } 53 | 54 | public BigDecimal getAmount() { 55 | return amount; 56 | } 57 | 58 | public void setAmount(BigDecimal amount) { 59 | this.amount = amount; 60 | } 61 | 62 | 63 | } 64 | -------------------------------------------------------------------------------- /orders-history-view-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordershistoryviewservice/backend/OrderViewRepository.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend; 2 | 3 | import com.mongodb.BasicDBObject; 4 | import com.mongodb.client.MongoClient; 5 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderState; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.OrderView; 8 | 9 | import java.util.Optional; 10 | 11 | public class OrderViewRepository extends AbstractRepository { 12 | 13 | public OrderViewRepository(MongoClient mongoClient) { 14 | super(mongoClient, "orders"); 15 | } 16 | 17 | public Optional findById(String orderId) { 18 | return findOne(orderId) 19 | .map(orderDocument -> { 20 | 21 | OrderView orderView = new OrderView(orderId, getMoney(orderDocument, "orderTotal")); 22 | 23 | Optional 24 | .ofNullable(orderDocument.getString("state")) 25 | .map(OrderState::valueOf).ifPresent(orderView::setState); 26 | 27 | return orderView; 28 | }); 29 | } 30 | 31 | public void addOrder(String orderId, Money orderTotal) { 32 | repeatOnFailure(() -> { 33 | findOneAndUpdate(orderId, new BasicDBObject("orderTotal", orderTotal.getAmount())); 34 | }); 35 | } 36 | 37 | public void updateOrderState(String orderId, OrderState state) { 38 | repeatOnFailure(() -> { 39 | findOneAndUpdate(orderId, new BasicDBObject("state", state.name())); 40 | }); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /orders-service/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/CustomerServiceProxyTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.client.RestTemplate; 8 | 9 | import static org.mockito.Mockito.mock; 10 | import static org.mockito.Mockito.verify; 11 | import static org.mockito.Mockito.when; 12 | 13 | public class CustomerServiceProxyTest { 14 | 15 | public static final String CUSTOMER_SERVICE_URL = "http://mycustomerservice/customers/{customerId}"; 16 | private static final String CUSTOMER_ID = "customer_id"; 17 | private RestTemplate restTemplate; 18 | private CustomerServiceProxy proxy; 19 | 20 | @Before 21 | public void setUp() { 22 | restTemplate = mock(RestTemplate.class); 23 | proxy = new CustomerServiceProxy(restTemplate); 24 | proxy.setCustomerServiceUrl(CUSTOMER_SERVICE_URL); 25 | } 26 | 27 | @Test 28 | public void shouldFindCustomer() { 29 | when(restTemplate.getForEntity(CUSTOMER_SERVICE_URL, Customer.class, CUSTOMER_ID)) 30 | .thenReturn(new ResponseEntity<>(HttpStatus.OK)); 31 | proxy.verifyCustomerCustomerId(CUSTOMER_ID); 32 | verify(restTemplate).getForEntity(CUSTOMER_SERVICE_URL, Customer.class, CUSTOMER_ID); 33 | } 34 | 35 | @Test(expected=CustomerNotFoundException.class) 36 | public void shouldNotFindCustomer() { 37 | when(restTemplate.getForEntity(CUSTOMER_SERVICE_URL, Customer.class, CUSTOMER_ID)) 38 | .thenReturn(new ResponseEntity<>(HttpStatus.NOT_FOUND)); 39 | proxy.verifyCustomerCustomerId(CUSTOMER_ID); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/Order.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | import io.eventuate.Event; 4 | import io.eventuate.ReflectiveMutableCommandProcessingAggregate; 5 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderApprovedEvent; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderCreatedEvent; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderRejectedEvent; 8 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderState; 9 | 10 | import java.util.List; 11 | 12 | import static io.eventuate.EventUtil.events; 13 | 14 | public class Order 15 | extends ReflectiveMutableCommandProcessingAggregate { 16 | 17 | private OrderState state; 18 | private String customerId; 19 | 20 | public List process(CreateOrderCommand cmd) { 21 | return events(new OrderCreatedEvent(cmd.getCustomerId(), cmd.getOrderTotal())); 22 | } 23 | 24 | public void apply(OrderCreatedEvent event) { 25 | this.state = OrderState.CREATED; 26 | this.customerId = event.getCustomerId(); 27 | } 28 | 29 | public OrderState getState() { 30 | return state; 31 | } 32 | 33 | public List process(ApproveOrderCommand cmd) { 34 | return events(new OrderApprovedEvent(customerId)); 35 | } 36 | 37 | public List process(RejectOrderCommand cmd) { 38 | return events(new OrderRejectedEvent(customerId)); 39 | } 40 | 41 | 42 | public void apply(OrderApprovedEvent event) { 43 | this.state = OrderState.APPROVED; 44 | } 45 | 46 | 47 | public void apply(OrderRejectedEvent event) { 48 | this.state = OrderState.REJECTED; 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /orders-service/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/CustomerServiceProxyIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | import io.micronaut.context.annotation.Value; 4 | import io.micronaut.test.annotation.MicronautTest; 5 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.customerscommon.CreateCustomerRequest; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.customerscommon.CreateCustomerResponse; 8 | import org.junit.jupiter.api.Assertions; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.ResponseEntity; 12 | import org.springframework.web.client.RestTemplate; 13 | 14 | import javax.inject.Inject; 15 | 16 | import static org.junit.Assert.assertEquals; 17 | 18 | @MicronautTest 19 | public class CustomerServiceProxyIntegrationTest { 20 | 21 | @Inject 22 | private CustomerServiceProxy customerServiceProxy; 23 | 24 | @Inject 25 | private RestTemplate restTemplate; 26 | 27 | @Value("${customer.service.root.url}") 28 | private String customerServiceRootUrl; 29 | 30 | @Test 31 | public void shouldVerifyExistingCustomer() { 32 | ResponseEntity response = restTemplate.postForEntity(customerServiceRootUrl, 33 | new CreateCustomerRequest("Fred", new Money(123)), CreateCustomerResponse.class); 34 | assertEquals(HttpStatus.OK, response.getStatusCode()); 35 | customerServiceProxy.verifyCustomerCustomerId(response.getBody().getCustomerId()); 36 | } 37 | 38 | @Test 39 | public void shouldRejectNonExistentCustomer() { 40 | Assertions.assertThrows(CustomerNotFoundException.class, () -> customerServiceProxy.verifyCustomerCustomerId("1223232-none")); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /customers-service/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/CustomerServiceInProcessComponentTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import com.jayway.restassured.RestAssured; 4 | import io.micronaut.context.ApplicationContext; 5 | import io.micronaut.runtime.server.EmbeddedServer; 6 | import io.micronaut.test.annotation.MicronautTest; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 8 | import net.chrisrichardson.eventstore.examples.customersandorders.customerscommon.CreateCustomerRequest; 9 | import org.junit.jupiter.api.Test; 10 | import static junit.framework.TestCase.assertNotNull; 11 | import static org.junit.Assert.assertEquals; 12 | 13 | @MicronautTest 14 | public class CustomerServiceInProcessComponentTest { 15 | 16 | private String baseUrl(int port, String path) { 17 | return "http://localhost:" + port + path; 18 | } 19 | 20 | @Test 21 | public void shouldCreateOrder() { 22 | EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer.class); 23 | 24 | int port = embeddedServer.getPort(); 25 | 26 | String postUrl = baseUrl(port,"/customers"); 27 | 28 | String customerId = RestAssured.given(). 29 | body(new CreateCustomerRequest("John Doe", new Money(1234))). 30 | contentType("application/json"). 31 | when(). 32 | post(postUrl). 33 | then(). 34 | statusCode(200). 35 | extract(). 36 | path("customerId"); 37 | 38 | assertNotNull(customerId); 39 | 40 | 41 | Integer creditLimit = RestAssured.given(). 42 | when(). 43 | get(postUrl + "/" + customerId). 44 | then(). 45 | statusCode(200). 46 | extract(). 47 | path("creditLimit.amount"); 48 | 49 | assertEquals(new Integer(1234), creditLimit); 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /customers-service/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/CustomerPropertyTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import com.pholser.junit.quickcheck.Property; 4 | import com.pholser.junit.quickcheck.generator.InRange; 5 | import com.pholser.junit.quickcheck.runner.JUnitQuickcheck; 6 | import io.eventuate.Aggregates; 7 | import io.eventuate.DefaultMissingApplyEventMethodStrategy; 8 | import io.eventuate.Event; 9 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreatedEvent; 10 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreditLimitExceededEvent; 11 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreditReservedEvent; 12 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 13 | import org.junit.runner.RunWith; 14 | 15 | import java.util.List; 16 | 17 | import static java.util.Collections.singletonList; 18 | import static org.junit.Assert.assertEquals; 19 | 20 | @RunWith(JUnitQuickcheck.class) 21 | public class CustomerPropertyTest { 22 | 23 | @Property 24 | public void reserveCredit(@InRange(minInt = 1) int orderTotal) { 25 | Customer customer = Aggregates.recreateAggregate(Customer.class, 26 | singletonList(new CustomerCreatedEvent(CustomerMother.name, CustomerMother.creditLimit)), DefaultMissingApplyEventMethodStrategy.INSTANCE); 27 | 28 | List events = customer.process(new ReserveCreditCommand(new Money(orderTotal), CustomerMother.orderId)); 29 | 30 | assertEquals(1, events.size()); 31 | Class eventClass = events.get(0).getClass(); 32 | 33 | if (CustomerMother.creditLimit.isGreaterThanOrEqual(new Money(orderTotal))) { 34 | assertEquals(CustomerCreditReservedEvent.class, eventClass); 35 | } else 36 | assertEquals(CustomerCreditLimitExceededEvent.class, eventClass); 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /customers-service/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/CustomerServiceTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import io.eventuate.EntityWithIdAndVersion; 4 | import io.eventuate.sync.AggregateRepository; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.mockito.ArgumentCaptor; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | import static org.junit.Assert.assertSame; 11 | import static org.mockito.Matchers.any; 12 | import static org.mockito.Mockito.mock; 13 | import static org.mockito.Mockito.verify; 14 | import static org.mockito.Mockito.verifyNoMoreInteractions; 15 | import static org.mockito.Mockito.when; 16 | 17 | public class CustomerServiceTest { 18 | 19 | private CustomerService customerService; 20 | private AggregateRepository aggregateRepository; 21 | 22 | @BeforeEach 23 | public void setUp() { 24 | aggregateRepository = mock(AggregateRepository.class); 25 | customerService = new CustomerServiceImpl(aggregateRepository); 26 | } 27 | 28 | @Test 29 | public void shouldCreateCustomer() { 30 | 31 | EntityWithIdAndVersion returned = new EntityWithIdAndVersion<>(null, null); 32 | 33 | when(aggregateRepository.save(any(CreateCustomerCommand.class))).thenReturn(returned); 34 | 35 | EntityWithIdAndVersion result = 36 | customerService.createCustomer(CustomerMother.name, CustomerMother.creditLimit); 37 | 38 | assertSame(returned, result); 39 | 40 | ArgumentCaptor argument = ArgumentCaptor.forClass(CreateCustomerCommand.class); 41 | 42 | verify(aggregateRepository).save(argument.capture()); 43 | verifyNoMoreInteractions(aggregateRepository); 44 | 45 | CreateCustomerCommand command = argument.getValue(); 46 | 47 | assertEquals(CustomerMother.name, command.getName()); 48 | assertEquals(CustomerMother.creditLimit, command.getCreditLimit()); 49 | 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /orders-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/CustomerServiceProxy.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | import io.micronaut.context.annotation.Value; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.util.Assert; 7 | import org.springframework.web.client.HttpClientErrorException; 8 | import org.springframework.web.client.RestTemplate; 9 | 10 | import javax.inject.Inject; 11 | import javax.inject.Singleton; 12 | 13 | @Singleton 14 | public class CustomerServiceProxy implements CustomerService { 15 | 16 | @Value("${customer.service.url}") 17 | private String customerServiceUrl; 18 | 19 | private RestTemplate restTemplate; 20 | 21 | @Inject 22 | public CustomerServiceProxy(RestTemplate restTemplate) { 23 | this.restTemplate = restTemplate; 24 | } 25 | 26 | public void setCustomerServiceUrl(String customerServiceUrl) { 27 | this.customerServiceUrl = customerServiceUrl; 28 | } 29 | 30 | @Override 31 | public void verifyCustomerCustomerId(String customerId) { 32 | Assert.notNull(customerServiceUrl, "Should not be null: " + customerServiceUrl); 33 | ResponseEntity result = null; 34 | try { 35 | result = restTemplate.getForEntity(customerServiceUrl, Customer.class, customerId); 36 | } catch (HttpClientErrorException e) { 37 | switch (e.getStatusCode()) { 38 | case NOT_FOUND: 39 | throw new CustomerNotFoundException(); 40 | default: 41 | unrecognizedStatusCode(customerId, e.getStatusCode()); 42 | } 43 | } 44 | switch (result.getStatusCode()) { 45 | case OK: 46 | return; 47 | case NOT_FOUND: 48 | throw new CustomerNotFoundException(); 49 | default: 50 | unrecognizedStatusCode(customerId, result.getStatusCode()); 51 | } 52 | } 53 | 54 | private void unrecognizedStatusCode(String customerId, HttpStatus statusCode) { 55 | throw new RuntimeException(String.format("Unrecognized status code %s when fetching customerId %s", 56 | statusCode, customerId)); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /customers-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/web/CustomerController.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.web; 2 | 3 | import io.eventuate.EntityNotFoundException; 4 | import io.eventuate.EntityWithIdAndVersion; 5 | import io.eventuate.EntityWithMetadata; 6 | import io.micronaut.http.HttpResponse; 7 | import io.micronaut.http.annotation.Controller; 8 | import io.micronaut.http.annotation.Get; 9 | import io.micronaut.http.annotation.Post; 10 | import net.chrisrichardson.eventstore.examples.customersandorders.customerscommon.CreateCustomerRequest; 11 | import net.chrisrichardson.eventstore.examples.customersandorders.customerscommon.CreateCustomerResponse; 12 | import net.chrisrichardson.eventstore.examples.customersandorders.customerscommon.GetCustomerResponse; 13 | import net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend.Customer; 14 | import net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend.CustomerService; 15 | 16 | import javax.inject.Inject; 17 | 18 | @Controller 19 | public class CustomerController { 20 | 21 | @Inject 22 | private CustomerService customerService; 23 | 24 | @Post(value = "/customers") 25 | public CreateCustomerResponse createCustomer(CreateCustomerRequest createCustomerRequest) { 26 | EntityWithIdAndVersion ewidv = customerService.createCustomer(createCustomerRequest.getName(), createCustomerRequest.getCreditLimit()); 27 | return new CreateCustomerResponse(ewidv.getEntityId()); 28 | } 29 | 30 | @Get("/customers/{customerId}") 31 | public HttpResponse getCustomer(String customerId) { 32 | EntityWithMetadata customerWithMetadata; 33 | try { 34 | customerWithMetadata = customerService.findById(customerId); 35 | } catch (EntityNotFoundException e) { 36 | return HttpResponse.notFound(); 37 | } 38 | 39 | Customer customer = customerWithMetadata.getEntity(); 40 | GetCustomerResponse response = 41 | new GetCustomerResponse(customerWithMetadata.getEntityIdAndVersion().getEntityId(), customer.getCreditLimit(), 42 | customer.availableCredit()); 43 | 44 | return HttpResponse.ok(response); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /customers-service/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "io.spring.dependency-management" 3 | id "java" 4 | id "com.github.johnrengelman.shadow" version "4.0.2" 5 | id "application" 6 | } 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencyManagement { 13 | imports { 14 | mavenBom "io.micronaut:micronaut-bom:$micronautVersion" 15 | } 16 | } 17 | 18 | dependencies { 19 | api project(":customers-common") 20 | 21 | api "io.eventuate.local.java:eventuate-client-java-micronaut" 22 | api "io.eventuate.local.java:eventuate-local-java-micronaut-jdbc" 23 | api "io.eventuate.common:eventuate-common-micronaut-data-jdbc:$eventuateCommonVersion" 24 | 25 | annotationProcessor "javax.persistence:javax.persistence-api:2.2" 26 | api 'io.micronaut.sql:micronaut-jdbc-hikari' 27 | annotationProcessor "io.micronaut:micronaut-inject-java" 28 | annotationProcessor "io.micronaut:micronaut-validation" 29 | api "io.micronaut:micronaut-inject" 30 | api "io.micronaut:micronaut-validation" 31 | api "io.micronaut:micronaut-runtime" 32 | api "io.micronaut:micronaut-http-server-netty" 33 | api 'io.micronaut:micronaut-management' 34 | runtimeOnly "ch.qos.logback:logback-classic:1.2.3" 35 | annotationProcessor "io.micronaut.configuration:micronaut-openapi" 36 | api "io.swagger.core.v3:swagger-annotations" 37 | api "io.eventuate.util:eventuate-util-swagger-ui:$eventuateUtilVersion" 38 | 39 | testImplementation 'com.jayway.restassured:rest-assured:2.9.0' 40 | testImplementation 'org.mockito:mockito-all:1.10.19' 41 | testImplementation 'com.pholser:junit-quickcheck-core:0.7' 42 | testImplementation 'com.pholser:junit-quickcheck-generators:0.7' 43 | testImplementation "io.eventuate.local.java:eventuate-client-java-micronaut-jdbc" 44 | testAnnotationProcessor "io.micronaut:micronaut-inject-java" 45 | testImplementation "org.junit.jupiter:junit-jupiter-api" 46 | testImplementation "io.micronaut.test:micronaut-test-junit5" 47 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" 48 | } 49 | 50 | mainClassName = "net.chrisrichardson.eventstore.examples.customersandorders.customersservice.CustomersServiceMain" 51 | 52 | shadowJar { 53 | mergeServiceFiles() 54 | } 55 | 56 | // use JUnit 5 platform 57 | test { 58 | useJUnitPlatform() 59 | } 60 | -------------------------------------------------------------------------------- /orders-history-view-service/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "io.spring.dependency-management" 3 | id "java" 4 | id "com.github.johnrengelman.shadow" version "4.0.2" 5 | id "application" 6 | } 7 | 8 | apply plugin: 'docker-compose' 9 | 10 | dockerCompose.isRequiredBy(test) 11 | 12 | dockerCompose { 13 | useComposeFiles = ['docker-compose-integration-tests.yml'] 14 | } 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | dependencyManagement { 21 | imports { 22 | mavenBom "io.micronaut:micronaut-bom:$micronautVersion" 23 | } 24 | } 25 | 26 | dependencies { 27 | api project(":orders-history-common") 28 | 29 | api "io.eventuate.local.java:eventuate-client-java-micronaut-events" 30 | api "io.eventuate.local.java:eventuate-local-java-micronaut-events" 31 | 32 | implementation("io.micronaut.mongodb:micronaut-mongo-sync") 33 | annotationProcessor "io.micronaut:micronaut-inject-java" 34 | annotationProcessor "io.micronaut:micronaut-validation" 35 | api "io.micronaut:micronaut-inject" 36 | api "io.micronaut:micronaut-validation" 37 | api "io.micronaut:micronaut-runtime" 38 | api "io.micronaut:micronaut-http-server-netty" 39 | api 'io.micronaut:micronaut-management' 40 | runtimeOnly "ch.qos.logback:logback-classic:1.2.3" 41 | annotationProcessor "io.micronaut.configuration:micronaut-openapi" 42 | api "io.swagger.core.v3:swagger-annotations" 43 | api "io.eventuate.util:eventuate-util-swagger-ui:$eventuateUtilVersion" 44 | 45 | testImplementation 'org.mockito:mockito-all:1.10.19' 46 | testImplementation project(":customers-common") 47 | testImplementation "junit:junit:4.12" 48 | testImplementation 'com.jayway.restassured:rest-assured:2.3.0' 49 | testImplementation "com.jayway.jsonpath:json-path:2.3.0" 50 | testAnnotationProcessor "io.micronaut:micronaut-inject-java" 51 | testImplementation "org.junit.jupiter:junit-jupiter-api" 52 | testImplementation "io.micronaut.test:micronaut-test-junit5" 53 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" 54 | } 55 | 56 | mainClassName = "net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.OrderHistoryQuerySideService" 57 | 58 | shadowJar { 59 | mergeServiceFiles() 60 | } 61 | 62 | // use JUnit 5 platform 63 | test { 64 | useJUnitPlatform() 65 | } 66 | -------------------------------------------------------------------------------- /customers-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/Customer.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import io.eventuate.Event; 4 | import io.eventuate.EventUtil; 5 | import io.eventuate.ReflectiveMutableCommandProcessingAggregate; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreatedEvent; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreditLimitExceededEvent; 8 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreditReservedEvent; 9 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 10 | 11 | import java.util.List; 12 | 13 | public class Customer extends ReflectiveMutableCommandProcessingAggregate { 14 | 15 | private ReservedCreditTracker reservedCreditTracker; 16 | 17 | private Money creditLimit; 18 | private String name; 19 | 20 | public Money availableCredit() { 21 | return creditLimit.subtract(reservedCreditTracker.reservedCredit()); 22 | } 23 | 24 | public Money getCreditLimit() { 25 | return creditLimit; 26 | } 27 | 28 | public List process(CreateCustomerCommand cmd) { 29 | return EventUtil.events(new CustomerCreatedEvent(cmd.getName(), cmd.getCreditLimit())); 30 | } 31 | 32 | public List process(ReserveCreditCommand cmd) { 33 | if (availableCredit().isGreaterThanOrEqual(cmd.getOrderTotal())) 34 | return EventUtil.events(new CustomerCreditReservedEvent(cmd.getOrderId(), cmd.getOrderTotal())); 35 | else 36 | return EventUtil.events(new CustomerCreditLimitExceededEvent(cmd.getOrderId())); 37 | } 38 | 39 | 40 | public void apply(CustomerCreatedEvent event) { 41 | this.name = name; 42 | this.creditLimit = event.getCreditLimit(); 43 | this.reservedCreditTracker = new ReservedCreditTracker(); 44 | } 45 | 46 | public void apply(CustomerCreditReservedEvent event) { 47 | reservedCreditTracker.addReservation(event.getOrderId(), event.getOrderTotal()); 48 | } 49 | 50 | public void apply(CustomerCreditLimitExceededEvent event) { 51 | // Do nothing 52 | } 53 | 54 | 55 | public String getName() { 56 | return name; 57 | } 58 | } 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /orders-service/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/ordersservice/backend/OrderTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.backend; 2 | 3 | 4 | import io.eventuate.Event; 5 | import io.eventuate.EventUtil; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderApprovedEvent; 8 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderCreatedEvent; 9 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderRejectedEvent; 10 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderState; 11 | import org.junit.Test; 12 | 13 | import java.util.List; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | 17 | public class OrderTest { 18 | 19 | Money orderTotal = new Money(10); 20 | String customerId = "customerID"; 21 | 22 | @Test 23 | public void testCreate() { 24 | Order order = new Order(); 25 | 26 | List events = order.process(new CreateOrderCommand(customerId, orderTotal)); 27 | 28 | assertEquals(EventUtil.events(new OrderCreatedEvent(customerId, orderTotal)), events); 29 | 30 | order.apply((OrderCreatedEvent) events.get(0)); 31 | 32 | assertEquals(OrderState.CREATED, order.getState()); 33 | 34 | } 35 | 36 | @Test 37 | public void testApprove() { 38 | Order order = new Order(); 39 | 40 | order.apply((OrderCreatedEvent) order.process(new CreateOrderCommand(customerId, orderTotal)).get(0)); 41 | 42 | List events = order.process(new ApproveOrderCommand()); 43 | 44 | assertEquals(EventUtil.events(new OrderApprovedEvent(customerId)), events); 45 | 46 | order.apply((OrderApprovedEvent) events.get(0)); 47 | 48 | assertEquals(OrderState.APPROVED, order.getState()); 49 | } 50 | 51 | @Test 52 | public void testReject() { 53 | Order order = new Order(); 54 | 55 | order.apply((OrderCreatedEvent) order.process(new CreateOrderCommand(customerId, orderTotal)).get(0)); 56 | 57 | List events = order.process(new RejectOrderCommand()); 58 | 59 | assertEquals(EventUtil.events(new OrderRejectedEvent(customerId)), events); 60 | 61 | order.apply((OrderRejectedEvent) events.get(0)); 62 | 63 | assertEquals(OrderState.REJECTED, order.getState()); 64 | 65 | } 66 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /orders-history-view-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordershistoryviewservice/backend/AbstractRepository.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend; 2 | 3 | import com.mongodb.BasicDBObject; 4 | import com.mongodb.MongoCommandException; 5 | import com.mongodb.client.MongoClient; 6 | import com.mongodb.client.MongoCollection; 7 | import com.mongodb.client.model.FindOneAndUpdateOptions; 8 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 9 | import org.bson.Document; 10 | import org.bson.types.Decimal128; 11 | 12 | import java.math.BigDecimal; 13 | import java.util.Optional; 14 | 15 | public class AbstractRepository { 16 | private static final String DATABASE = "customers_and_orders"; 17 | private static final int REPEAT_ON_FAILURE_COUNT = 10; 18 | private final String collection; 19 | 20 | private MongoClient mongoClient; 21 | 22 | public AbstractRepository(MongoClient mongoClient, String collection) { 23 | this.mongoClient = mongoClient; 24 | this.collection = collection; 25 | } 26 | 27 | protected void findOneAndUpdate(String id, BasicDBObject newValue) { 28 | collection().findOneAndUpdate(new BasicDBObject("_id", id), 29 | new BasicDBObject("$set", newValue), 30 | upsertOptions()); 31 | } 32 | 33 | protected Optional findOne(String id) { 34 | Document customerDocument = collection() 35 | .find(new BasicDBObject("_id", id)) 36 | .first(); 37 | 38 | return Optional 39 | .ofNullable(customerDocument); 40 | } 41 | 42 | protected FindOneAndUpdateOptions upsertOptions() { 43 | FindOneAndUpdateOptions opts = new FindOneAndUpdateOptions(); 44 | 45 | opts.upsert(true); 46 | 47 | return opts; 48 | } 49 | 50 | protected MongoCollection collection() { 51 | return mongoClient 52 | .getDatabase(DATABASE) 53 | .getCollection(collection); 54 | } 55 | 56 | protected Money getMoney(Document document, String field) { 57 | BigDecimal bigDecimal = Optional 58 | .ofNullable(document.get(field, Decimal128.class)) 59 | .map(Decimal128::bigDecimalValue) 60 | .orElse(new BigDecimal(0)); 61 | 62 | return new Money(bigDecimal); 63 | } 64 | 65 | protected void repeatOnFailure(Runnable action) { 66 | MongoCommandException mongoCommandException = null; 67 | 68 | for (int i = 0; i < REPEAT_ON_FAILURE_COUNT; i++) { 69 | try { 70 | action.run(); 71 | return; 72 | } catch (MongoCommandException e) { 73 | mongoCommandException = e; 74 | } 75 | } 76 | 77 | throw new RuntimeException(mongoCommandException); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /orders-history-view-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordershistoryviewservice/backend/OrderHistoryViewWorkflow.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend; 2 | 3 | import io.eventuate.DispatchedEvent; 4 | import io.eventuate.EventHandlerMethod; 5 | import io.eventuate.EventSubscriber; 6 | import io.eventuate.Subscriber; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreatedEvent; 8 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 9 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderApprovedEvent; 10 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderCreatedEvent; 11 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderRejectedEvent; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | 15 | @EventSubscriber(id = "orderHistoryWorkflow") 16 | public class OrderHistoryViewWorkflow implements Subscriber { 17 | private Logger logger = LoggerFactory.getLogger(getClass()); 18 | 19 | private OrderHistoryViewService orderHistoryViewService; 20 | 21 | public OrderHistoryViewWorkflow(OrderHistoryViewService orderHistoryViewService) { 22 | this.orderHistoryViewService = orderHistoryViewService; 23 | } 24 | 25 | @EventHandlerMethod 26 | public void createCustomer(DispatchedEvent de) { 27 | System.out.println(String.format("createCustomer invoked %s", de)); 28 | 29 | String customerId = de.getEntityId(); 30 | orderHistoryViewService.createCustomer(customerId, de.getEvent().getName(), 31 | de.getEvent().getCreditLimit()); 32 | } 33 | 34 | @EventHandlerMethod 35 | public void createOrder(DispatchedEvent de) { 36 | System.out.println(String.format("createOrder invoked %s", de)); 37 | 38 | String customerId = de.getEvent().getCustomerId(); 39 | String orderId = de.getEntityId(); 40 | Money orderTotal = de.getEvent().getOrderTotal(); 41 | orderHistoryViewService.addOrder(customerId, orderId, orderTotal); 42 | } 43 | 44 | @EventHandlerMethod 45 | public void orderApproved(DispatchedEvent de) { 46 | System.out.println(String.format("orderApproved invoked %s", de)); 47 | 48 | String customerId = de.getEvent().getCustomerId(); 49 | String orderId = de.getEntityId(); 50 | orderHistoryViewService.approveOrder(customerId, orderId); } 51 | 52 | @EventHandlerMethod 53 | public void orderRejected(DispatchedEvent de) { 54 | System.out.println(String.format("orderRejected invoked %s", de)); 55 | 56 | String customerId = de.getEvent().getCustomerId(); 57 | String orderId = de.getEntityId(); 58 | orderHistoryViewService.rejectOrder(customerId, orderId); 59 | } 60 | 61 | 62 | } 63 | -------------------------------------------------------------------------------- /customers-service/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/customersservice/backend/CustomerTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.customersservice.backend; 2 | 3 | import io.eventuate.Aggregates; 4 | import io.eventuate.DefaultMissingApplyEventMethodStrategy; 5 | import io.eventuate.Event; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreatedEvent; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreditLimitExceededEvent; 8 | import net.chrisrichardson.eventstore.examples.customersandorders.common.customer.CustomerCreditReservedEvent; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import java.util.List; 13 | 14 | import static io.eventuate.EventUtil.events; 15 | import static org.junit.Assert.assertEquals; 16 | 17 | public class CustomerTest { 18 | 19 | private Customer customer; 20 | private List events; 21 | 22 | @BeforeEach 23 | public void createEmptyCustomer() { 24 | customer = new Customer(); 25 | } 26 | 27 | @Test 28 | public void testCreate() { 29 | 30 | process(new CreateCustomerCommand(CustomerMother.name, CustomerMother.creditLimit)); 31 | 32 | assertEventEquals(new CustomerCreatedEvent(CustomerMother.name, CustomerMother.creditLimit)); 33 | 34 | applyEventsToMutableAggregate(); 35 | 36 | assertEquals(CustomerMother.creditLimit, customer.getCreditLimit()); 37 | assertEquals(CustomerMother.creditLimit, customer.availableCredit()); 38 | } 39 | 40 | private void applyEventsToMutableAggregate() { 41 | Aggregates.applyEventsToMutableAggregate(customer, events, DefaultMissingApplyEventMethodStrategy.INSTANCE); 42 | } 43 | 44 | 45 | @Test 46 | public void testReserveCredit() { 47 | initializeCustomer(); 48 | 49 | process(new ReserveCreditCommand(CustomerMother.orderTotalWithinCreditLimit, CustomerMother.orderId)); 50 | 51 | assertEventEquals(new CustomerCreditReservedEvent(CustomerMother.orderId, CustomerMother.orderTotalWithinCreditLimit)); 52 | 53 | applyEventsToMutableAggregate(); 54 | 55 | assertEquals(CustomerMother.creditLimit, customer.getCreditLimit()); 56 | assertEquals(CustomerMother.creditLimit.subtract(CustomerMother.orderTotalWithinCreditLimit), customer.availableCredit()); 57 | 58 | } 59 | 60 | 61 | @Test 62 | public void testCreditLimitExceeded() { 63 | initializeCustomer(); 64 | 65 | process(new ReserveCreditCommand(CustomerMother.orderTotalThatExceedsCreditLimit, CustomerMother.orderId)); 66 | 67 | assertEventEquals(new CustomerCreditLimitExceededEvent(CustomerMother.orderId)); 68 | 69 | applyEventsToMutableAggregate(); 70 | 71 | assertEquals(CustomerMother.creditLimit, customer.getCreditLimit()); 72 | assertEquals(CustomerMother.creditLimit, customer.availableCredit()); 73 | } 74 | 75 | private void process(T command) { 76 | events = customer.processCommand(command); 77 | } 78 | 79 | 80 | private void assertEventEquals(Event expectedEvent) { 81 | assertEquals(events(expectedEvent), events); 82 | } 83 | 84 | private void initializeCustomer() { 85 | process(new CreateCustomerCommand(CustomerMother.name, CustomerMother.creditLimit)); 86 | applyEventsToMutableAggregate(); 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /orders-history-view-service/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/ordershistoryviewservice/backend/CustomerViewRepository.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend; 2 | 3 | import com.mongodb.BasicDBObject; 4 | import com.mongodb.client.MongoClient; 5 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderState; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.CustomerView; 8 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.OrderInfo; 9 | import org.bson.Document; 10 | import java.util.Optional; 11 | 12 | public class CustomerViewRepository extends AbstractRepository { 13 | 14 | public CustomerViewRepository(MongoClient mongoClient) { 15 | super(mongoClient, "customers"); 16 | } 17 | 18 | public Optional findById(String customerId) { 19 | return findOne(customerId) 20 | .map(customerDocument -> { 21 | CustomerView customerView = new CustomerView(); 22 | 23 | customerView.setId(customerId); 24 | customerView.setName(customerDocument.getString("name")); 25 | customerView.setCreditLimit(getMoney(customerDocument, "creditLimit")); 26 | 27 | Document orders = customerDocument.get("orders", Document.class); 28 | 29 | if (orders != null) { 30 | orders.forEach((key, value) -> { 31 | String orderId = key; 32 | 33 | Document orderDocument = (Document) value; 34 | 35 | OrderInfo orderInfo = new OrderInfo(orderId, getMoney(orderDocument, "orderTotal")); 36 | 37 | Optional 38 | .ofNullable(orderDocument.getString("state")) 39 | .map(OrderState::valueOf) 40 | .ifPresent(orderInfo::setState); 41 | 42 | customerView.getOrders().put(orderId, orderInfo); 43 | }); 44 | } 45 | 46 | return customerView; 47 | }); 48 | } 49 | 50 | public void addCustomer(String customerId, String customerName, Money creditLimit) { 51 | repeatOnFailure(() -> { 52 | BasicDBObject customer = new BasicDBObject() 53 | .append("name", customerName) 54 | .append("creditLimit", creditLimit.getAmount()); 55 | 56 | findOneAndUpdate(customerId, customer); 57 | }); 58 | } 59 | 60 | public void addOrder(String customerId, String orderId, Money orderTotal) { 61 | repeatOnFailure(() -> { 62 | BasicDBObject orderInfo = new BasicDBObject() 63 | .append("orderId", orderId) 64 | .append("orderTotal", orderTotal.getAmount()); 65 | 66 | findOneAndUpdate(customerId, new BasicDBObject("orders." + orderId, orderInfo)); 67 | }); 68 | } 69 | 70 | public void updateOrderState(String customerId, String orderId, OrderState state) { 71 | repeatOnFailure(() -> { 72 | collection().findOneAndUpdate(new BasicDBObject("_id", customerId), 73 | new BasicDBObject("$set", new BasicDBObject(String.format("orders.%s.state", orderId), state.name())), 74 | upsertOptions()); 75 | }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /orders-service/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "io.spring.dependency-management" 3 | id "java" 4 | id "com.github.johnrengelman.shadow" version "4.0.2" 5 | id "application" 6 | } 7 | 8 | apply plugin: 'docker-compose' 9 | 10 | dockerCompose.isRequiredBy(test) 11 | 12 | dockerCompose { 13 | environment.put "EVENTUATE_COMMON_VERSION", eventuateCommonImageVersion 14 | environment.put "EVENTUATE_MESSAGING_KAFKA_IMAGE_VERSION", eventuateMessagingKafkaImageVersion 15 | environment.put "EVENTUATE_CDC_VERSION", eventuateCdcImageVersion 16 | environment.put "EVENTUATE_JAVA_BASE_IMAGE_VERSION", eventuateExamplesBaseImageVersion 17 | 18 | if ("EventuatePolling".equals(System.getenv("SPRING_PROFILES_ACTIVE"))) 19 | useComposeFiles = ['../docker-compose-eventuate-local-postgres-polling.yml'] 20 | else if ("PostgresWal".equals(System.getenv("SPRING_PROFILES_ACTIVE"))) 21 | useComposeFiles = ['../docker-compose-eventuate-local-postgres-wal.yml'] 22 | else 23 | useComposeFiles = ['../docker-compose-eventuate-local-mysql.yml'] 24 | startedServices=["customercommandside"] 25 | projectName = null 26 | 27 | } 28 | 29 | repositories { 30 | mavenCentral() 31 | } 32 | 33 | dependencyManagement { 34 | imports { 35 | mavenBom "io.micronaut:micronaut-bom:$micronautVersion" 36 | } 37 | } 38 | 39 | dependencies { 40 | api project(":orders-common") 41 | api "org.springframework.boot:spring-boot-starter-web:$springBootVersion" 42 | 43 | api "io.eventuate.local.java:eventuate-client-java-micronaut" 44 | api "io.eventuate.local.java:eventuate-local-java-micronaut-jdbc" 45 | api "io.eventuate.common:eventuate-common-micronaut-data-jdbc:$eventuateCommonVersion" 46 | 47 | annotationProcessor "javax.persistence:javax.persistence-api:2.2" 48 | api 'io.micronaut.sql:micronaut-jdbc-hikari' 49 | annotationProcessor "io.micronaut:micronaut-inject-java" 50 | annotationProcessor "io.micronaut:micronaut-validation" 51 | api "io.micronaut:micronaut-inject" 52 | api "io.micronaut:micronaut-validation" 53 | api "io.micronaut:micronaut-runtime" 54 | api "io.micronaut:micronaut-http-server-netty" 55 | api 'io.micronaut:micronaut-management' 56 | runtimeOnly "ch.qos.logback:logback-classic:1.2.3" 57 | annotationProcessor "io.micronaut.configuration:micronaut-openapi" 58 | api "io.swagger.core.v3:swagger-annotations" 59 | api "io.eventuate.util:eventuate-util-swagger-ui:$eventuateUtilVersion" 60 | 61 | testImplementation 'org.mockito:mockito-all:1.10.19' 62 | testImplementation "io.eventuate.local.java:eventuate-client-java-micronaut-jdbc" 63 | testImplementation project(":customers-common") 64 | testImplementation "junit:junit:4.12" 65 | testImplementation 'com.jayway.restassured:rest-assured:2.3.0' 66 | testImplementation "com.jayway.jsonpath:json-path:2.3.0" 67 | testAnnotationProcessor "io.micronaut:micronaut-inject-java" 68 | testImplementation "org.junit.jupiter:junit-jupiter-api" 69 | testImplementation "io.micronaut.test:micronaut-test-junit5" 70 | testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine" 71 | } 72 | 73 | mainClassName = "net.chrisrichardson.eventstore.examples.customersandorders.ordersservice.OrdersServiceMain" 74 | 75 | shadowJar { 76 | mergeServiceFiles() 77 | } 78 | 79 | // use JUnit 5 platform 80 | test { 81 | useJUnitPlatform() 82 | } 83 | -------------------------------------------------------------------------------- /orders-history-view-service/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/views/orderhistory/OrderHistoryViewServiceTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.views.orderhistory; 2 | 3 | import io.micronaut.test.annotation.MicronautTest; 4 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 5 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderState; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.CustomerView; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.OrderView; 8 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend.CustomerViewRepository; 9 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend.OrderHistoryViewService; 10 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistoryviewservice.backend.OrderViewRepository; 11 | import org.junit.jupiter.api.Test; 12 | 13 | import javax.inject.Inject; 14 | import java.util.UUID; 15 | 16 | import static org.junit.Assert.assertEquals; 17 | import static org.junit.Assert.assertNotNull; 18 | 19 | @MicronautTest 20 | public class OrderHistoryViewServiceTest { 21 | 22 | @Inject 23 | private OrderHistoryViewService orderHistoryViewService; 24 | 25 | @Inject 26 | private CustomerViewRepository customerViewRepository; 27 | 28 | @Inject 29 | private OrderViewRepository orderViewRepository; 30 | 31 | @Test 32 | public void shouldCreateCustomerAndOrdersEtc() { 33 | String customerId = UUID.randomUUID().toString(); 34 | Money creditLimit = new Money(2000); 35 | String customerName = "Fred"; 36 | 37 | String orderId1 = UUID.randomUUID().toString(); 38 | Money orderTotal1 = new Money(1234); 39 | String orderId2 = UUID.randomUUID().toString(); 40 | Money orderTotal2 = new Money(3000); 41 | 42 | orderHistoryViewService.createCustomer(customerId, customerName, creditLimit); 43 | orderHistoryViewService.addOrder(customerId, orderId1, orderTotal1); 44 | orderHistoryViewService.approveOrder(customerId, orderId1); 45 | 46 | orderHistoryViewService.addOrder(customerId, orderId2, orderTotal2); 47 | orderHistoryViewService.rejectOrder(customerId, orderId2); 48 | 49 | CustomerView customerView = customerViewRepository.findById(customerId).get(); 50 | 51 | 52 | assertEquals(2, customerView.getOrders().size()); 53 | assertNotNull(customerView.getOrders().get(orderId1)); 54 | assertNotNull(customerView.getOrders().get(orderId2)); 55 | assertEquals(orderTotal1, customerView.getOrders().get(orderId1).getOrderTotal()); 56 | assertEquals(OrderState.APPROVED, customerView.getOrders().get(orderId1).getState()); 57 | 58 | assertNotNull(customerView.getOrders().get(orderId2)); 59 | assertEquals(orderTotal2, customerView.getOrders().get(orderId2).getOrderTotal()); 60 | assertEquals(OrderState.REJECTED, customerView.getOrders().get(orderId2).getState()); 61 | 62 | OrderView orderView1 = orderViewRepository.findById(orderId1).get(); 63 | assertEquals(orderTotal1, orderView1.getOrderTotal()); 64 | assertEquals(OrderState.APPROVED, orderView1.getState()); 65 | 66 | OrderView orderView2 = orderViewRepository.findById(orderId2).get(); 67 | assertEquals(orderTotal2, orderView2.getOrderTotal()); 68 | assertEquals(OrderState.REJECTED, orderView2.getState()); 69 | } 70 | 71 | 72 | } -------------------------------------------------------------------------------- /docker-compose-eventuate-local-postgres-wal.yml: -------------------------------------------------------------------------------- 1 | services: 2 | zookeeper: 3 | image: eventuateio/eventuate-zookeeper:$EVENTUATE_COMMON_VERSION 4 | ports: 5 | - 2181:2181 6 | environment: 7 | ZOOKEEPER_CLIENT_PORT: 2181 8 | KAFKA_HEAP_OPTS: -Xmx64m 9 | 10 | kafka: 11 | image: "eventuateio/eventuate-kafka:$EVENTUATE_MESSAGING_KAFKA_IMAGE_VERSION" 12 | ports: 13 | - 9092:9092 14 | - 29092:29092 15 | depends_on: 16 | - zookeeper 17 | environment: 18 | KAFKA_LISTENERS: LC://kafka:29092,LX://kafka:9092 19 | KAFKA_ADVERTISED_LISTENERS: LC://kafka:29092,LX://${DOCKER_HOST_IP:-localhost}:9092 20 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LC:PLAINTEXT,LX:PLAINTEXT 21 | KAFKA_INTER_BROKER_LISTENER_NAME: LC 22 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 23 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 24 | KAFKA_HEAP_OPTS: -Xmx192m 25 | 26 | postgres: 27 | image: eventuateio/eventuate-postgres:$EVENTUATE_COMMON_VERSION 28 | ports: 29 | - 5432:5432 30 | environment: 31 | POSTGRES_USER: eventuate 32 | POSTGRES_PASSWORD: eventuate 33 | 34 | 35 | cdcservice: 36 | image: eventuateio/eventuate-cdc-service:$EVENTUATE_CDC_VERSION 37 | ports: 38 | - "8099:8080" 39 | depends_on: 40 | - postgres 41 | - kafka 42 | - zookeeper 43 | environment: 44 | SPRING_DATASOURCE_URL: jdbc:postgresql://postgres/eventuate 45 | SPRING_DATASOURCE_USERNAME: eventuate 46 | SPRING_DATASOURCE_PASSWORD: eventuate 47 | SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver 48 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 49 | EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING: zookeeper:2181 50 | SPRING_PROFILES_ACTIVE: PostgresWal 51 | EVENTUATELOCAL_CDC_READER_NAME: PostgresWalReader 52 | EVENTUATE_CDC_TYPE: "EventuateLocal" 53 | EVENTUATE_OUTBOX_ID: 1 54 | JAVA_OPTS: -Xmx64m 55 | 56 | mongodb: 57 | image: mongo:4.2.12 58 | hostname: mongodb 59 | ports: 60 | - "27017:27017" 61 | 62 | customercommandside: 63 | build: 64 | context: ./customers-service/ 65 | args: 66 | baseImageVersion: ${EVENTUATE_JAVA_BASE_IMAGE_VERSION?} 67 | ports: 68 | - "8081:8080" 69 | depends_on: 70 | - postgres 71 | - kafka 72 | - zookeeper 73 | environment: 74 | DB_URL: jdbc:postgresql://postgres/eventuate 75 | DB_USERNAME: eventuate 76 | DB_PASSWORD: eventuate 77 | DB_DRIVERCLASSNAME: org.postgresql.Driver 78 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 79 | 80 | orderhistoryqueryside: 81 | build: 82 | context: ./orders-history-view-service/ 83 | args: 84 | baseImageVersion: ${EVENTUATE_JAVA_BASE_IMAGE_VERSION?} 85 | ports: 86 | - "8082:8080" 87 | depends_on: 88 | - mongodb 89 | - postgres 90 | - kafka 91 | - zookeeper 92 | environment: 93 | SPRING_DATA_MONGODB_URI: mongodb://mongodb/customers_and_orders 94 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 95 | 96 | ordercommandside: 97 | build: 98 | context: ./orders-service/ 99 | args: 100 | baseImageVersion: ${EVENTUATE_JAVA_BASE_IMAGE_VERSION?} 101 | ports: 102 | - "8083:8080" 103 | depends_on: 104 | - postgres 105 | - kafka 106 | - zookeeper 107 | - customercommandside 108 | environment: 109 | CUSTOMER_SERVICE_URL: http://customercommandside:8080/customers/{customerId} 110 | ENDPOINTS_SENSITIVE: "false" 111 | DB_URL: jdbc:postgresql://postgres/eventuate 112 | DB_USERNAME: eventuate 113 | DB_PASSWORD: eventuate 114 | DB_DRIVERCLASSNAME: org.postgresql.Driver 115 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 116 | -------------------------------------------------------------------------------- /docker-compose-eventuate-local-postgres-polling.yml: -------------------------------------------------------------------------------- 1 | services: 2 | zookeeper: 3 | image: eventuateio/eventuate-zookeeper:$EVENTUATE_COMMON_VERSION 4 | ports: 5 | - 2181:2181 6 | environment: 7 | ZOOKEEPER_CLIENT_PORT: 2181 8 | KAFKA_HEAP_OPTS: -Xmx64m 9 | 10 | kafka: 11 | image: "eventuateio/eventuate-kafka:$EVENTUATE_MESSAGING_KAFKA_IMAGE_VERSION" 12 | ports: 13 | - 9092:9092 14 | - 29092:29092 15 | depends_on: 16 | - zookeeper 17 | environment: 18 | KAFKA_LISTENERS: LC://kafka:29092,LX://kafka:9092 19 | KAFKA_ADVERTISED_LISTENERS: LC://kafka:29092,LX://${DOCKER_HOST_IP:-localhost}:9092 20 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LC:PLAINTEXT,LX:PLAINTEXT 21 | KAFKA_INTER_BROKER_LISTENER_NAME: LC 22 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 23 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 24 | KAFKA_HEAP_OPTS: -Xmx192m 25 | 26 | postgres: 27 | image: eventuateio/eventuate-postgres:$EVENTUATE_COMMON_VERSION 28 | ports: 29 | - 5432:5432 30 | environment: 31 | POSTGRES_USER: eventuate 32 | POSTGRES_PASSWORD: eventuate 33 | 34 | 35 | cdcservice: 36 | image: eventuateio/eventuate-cdc-service:$EVENTUATE_CDC_VERSION 37 | ports: 38 | - "8099:8080" 39 | depends_on: 40 | - postgres 41 | - kafka 42 | - zookeeper 43 | environment: 44 | SPRING_DATASOURCE_URL: jdbc:postgresql://postgres/eventuate 45 | SPRING_DATASOURCE_USERNAME: eventuate 46 | SPRING_DATASOURCE_PASSWORD: eventuate 47 | SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver 48 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 49 | EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING: zookeeper:2181 50 | SPRING_PROFILES_ACTIVE: EventuatePolling 51 | EVENTUATELOCAL_CDC_READER_NAME: PostgresPollingReader 52 | EVENTUATE_CDC_TYPE: "EventuateLocal" 53 | EVENTUATE_OUTBOX_ID: 1 54 | JAVA_OPTS: -Xmx64m 55 | 56 | mongodb: 57 | image: mongo:4.2.12 58 | hostname: mongodb 59 | ports: 60 | - "27017:27017" 61 | 62 | customercommandside: 63 | build: 64 | context: ./customers-service/ 65 | args: 66 | baseImageVersion: ${EVENTUATE_JAVA_BASE_IMAGE_VERSION?} 67 | ports: 68 | - "8081:8080" 69 | depends_on: 70 | - postgres 71 | - kafka 72 | - zookeeper 73 | environment: 74 | DB_URL: jdbc:postgresql://postgres/eventuate 75 | DB_USERNAME: eventuate 76 | DB_PASSWORD: eventuate 77 | DB_DRIVERCLASSNAME: org.postgresql.Driver 78 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 79 | SPRING_PROFILES_ACTIVE: EventuatePolling 80 | EVENTUATELOCAL_CDC_READER_NAME: PostgresPollingReader 81 | 82 | orderhistoryqueryside: 83 | build: 84 | context: ./orders-history-view-service/ 85 | args: 86 | baseImageVersion: ${EVENTUATE_JAVA_BASE_IMAGE_VERSION?} 87 | ports: 88 | - "8082:8080" 89 | depends_on: 90 | - mongodb 91 | - postgres 92 | - kafka 93 | - zookeeper 94 | environment: 95 | SPRING_DATA_MONGODB_URI: mongodb://mongodb/customers_and_orders 96 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 97 | 98 | ordercommandside: 99 | build: 100 | context: ./orders-service/ 101 | args: 102 | baseImageVersion: ${EVENTUATE_JAVA_BASE_IMAGE_VERSION?} 103 | ports: 104 | - "8083:8080" 105 | depends_on: 106 | - postgres 107 | - kafka 108 | - zookeeper 109 | - customercommandside 110 | environment: 111 | CUSTOMER_SERVICE_URL: http://customercommandside:8080/customers/{customerId} 112 | ENDPOINTS_SENSITIVE: "false" 113 | DB_URL: jdbc:postgresql://postgres/eventuate 114 | DB_USERNAME: eventuate 115 | DB_PASSWORD: eventuate 116 | DB_DRIVERCLASSNAME: org.postgresql.Driver 117 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 118 | -------------------------------------------------------------------------------- /docker-compose-eventuate-local-mysql.yml: -------------------------------------------------------------------------------- 1 | services: 2 | zookeeper: 3 | image: eventuateio/eventuate-zookeeper:$EVENTUATE_COMMON_VERSION 4 | ports: 5 | - 2181:2181 6 | environment: 7 | ZOOKEEPER_CLIENT_PORT: 2181 8 | KAFKA_HEAP_OPTS: -Xmx64m 9 | 10 | kafka: 11 | image: "eventuateio/eventuate-kafka:$EVENTUATE_MESSAGING_KAFKA_IMAGE_VERSION" 12 | ports: 13 | - 9092:9092 14 | - 29092:29092 15 | depends_on: 16 | - zookeeper 17 | environment: 18 | KAFKA_LISTENERS: LC://kafka:29092,LX://kafka:9092 19 | KAFKA_ADVERTISED_LISTENERS: LC://kafka:29092,LX://${DOCKER_HOST_IP:-localhost}:9092 20 | KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LC:PLAINTEXT,LX:PLAINTEXT 21 | KAFKA_INTER_BROKER_LISTENER_NAME: LC 22 | KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 23 | KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 24 | KAFKA_HEAP_OPTS: -Xmx192m 25 | 26 | mysql: 27 | image: eventuateio/eventuate-mysql8:$EVENTUATE_COMMON_VERSION 28 | ports: 29 | - 3306:3306 30 | environment: 31 | - MYSQL_ROOT_PASSWORD=rootpassword 32 | - MYSQL_USER=mysqluser 33 | - MYSQL_PASSWORD=mysqlpw 34 | 35 | cdcservice: 36 | image: eventuateio/eventuate-cdc-service:$EVENTUATE_CDC_VERSION 37 | ports: 38 | - "8099:8080" 39 | depends_on: 40 | - mysql 41 | - kafka 42 | - zookeeper 43 | environment: 44 | SPRING_DATASOURCE_URL: jdbc:mysql://mysql/eventuate 45 | SPRING_DATASOURCE_USERNAME: mysqluser 46 | SPRING_DATASOURCE_PASSWORD: mysqlpw 47 | SPRING_DATASOURCE_DRIVER_CLASS_NAME: com.mysql.jdbc.Driver 48 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 49 | EVENTUATELOCAL_ZOOKEEPER_CONNECTION_STRING: zookeeper:2181 50 | EVENTUATELOCAL_CDC_DB_USER_NAME: root 51 | EVENTUATELOCAL_CDC_DB_PASSWORD: rootpassword 52 | EVENTUATELOCAL_CDC_READER_NAME: MySqlReader 53 | EVENTUATELOCAL_CDC_MYSQL_BINLOG_CLIENT_UNIQUE_ID: 1234567890 54 | EVENTUATELOCAL_CDC_READ_OLD_DEBEZIUM_DB_OFFSET_STORAGE_TOPIC: "false" 55 | EVENTUATE_CDC_TYPE: "EventuateLocal" 56 | EVENTUATE_OUTBOX_ID: 1 57 | JAVA_OPTS: -Xmx64m 58 | 59 | mongodb: 60 | image: mongo:4.2.12 61 | hostname: mongodb 62 | ports: 63 | - "27017:27017" 64 | 65 | customercommandside: 66 | build: 67 | context: ./customers-service/ 68 | args: 69 | baseImageVersion: ${EVENTUATE_JAVA_BASE_IMAGE_VERSION} 70 | ports: 71 | - "8081:8080" 72 | depends_on: 73 | - mysql 74 | - kafka 75 | - zookeeper 76 | environment: 77 | DB_URL: jdbc:mysql://mysql/eventuate 78 | DB_DRIVERCLASSNAME: com.mysql.jdbc.Driver 79 | DB_USERNAME: mysqluser 80 | DB_PASSWORD: mysqlpw 81 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 82 | 83 | orderhistoryqueryside: 84 | build: 85 | context: ./orders-history-view-service/ 86 | args: 87 | baseImageVersion: ${EVENTUATE_JAVA_BASE_IMAGE_VERSION} 88 | ports: 89 | - "8082:8080" 90 | depends_on: 91 | - mongodb 92 | - mysql 93 | - kafka 94 | - zookeeper 95 | environment: 96 | SPRING_DATA_MONGODB_URI: mongodb://mongodb/customers_and_orders 97 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 98 | 99 | ordercommandside: 100 | build: 101 | context: ./orders-service/ 102 | args: 103 | baseImageVersion: ${EVENTUATE_JAVA_BASE_IMAGE_VERSION} 104 | ports: 105 | - "8083:8080" 106 | depends_on: 107 | - mysql 108 | - kafka 109 | - zookeeper 110 | - customercommandside 111 | environment: 112 | CUSTOMER_SERVICE_URL: http://customercommandside:8080/customers/{customerId} 113 | ENDPOINTS_SENSITIVE: "false" 114 | DB_URL: jdbc:mysql://mysql/eventuate 115 | DB_DRIVERCLASSNAME: com.mysql.jdbc.Driver 116 | DB_USERNAME: mysqluser 117 | DB_PASSWORD: mysqlpw 118 | EVENTUATELOCAL_KAFKA_BOOTSTRAP_SERVERS: kafka:29092 119 | -------------------------------------------------------------------------------- /common-test/src/main/java/net/chrisrichardson/eventstore/examples/customersandorders/commontest/AbstractCustomerAndOrdersIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.commontest; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | import net.chrisrichardson.eventstore.examples.customersandorders.common.order.OrderState; 5 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.CustomerView; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.OrderInfo; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.OrderView; 8 | import org.junit.Test; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import static io.eventuate.util.test.async.Eventually.eventually; 15 | import static io.eventuate.util.test.async.Eventually.eventuallyReturning; 16 | import static org.junit.Assert.assertEquals; 17 | import static org.junit.Assert.assertNotNull; 18 | import static org.junit.Assert.fail; 19 | 20 | public abstract class AbstractCustomerAndOrdersIntegrationTest { 21 | protected Logger logger = LoggerFactory.getLogger(getClass()); 22 | 23 | private final Money creditLimit = new Money(1000); 24 | 25 | protected class IntegrationTestCustomerNotFoundException extends RuntimeException { 26 | public IntegrationTestCustomerNotFoundException(Throwable cause) { 27 | super(cause); 28 | } 29 | } 30 | 31 | @Test 32 | public void shouldCreateAndApproveOrder() throws Exception { 33 | 34 | String customerId = createCustomer(creditLimit); 35 | 36 | Thread.sleep(4000); 37 | 38 | Money orderTotal = new Money(720); 39 | 40 | String orderId = createOrder(customerId, orderTotal); 41 | 42 | eventually(40, 500, TimeUnit.MILLISECONDS, () -> { 43 | OrderView o = getOrderView(orderId); 44 | assertNotNull(o); 45 | assertEquals(OrderState.APPROVED, o.getState()); 46 | }); 47 | 48 | CustomerView customerView = eventuallyReturning(() -> { 49 | CustomerView cv = getCustomerView(customerId); 50 | assertNotNull(cv); 51 | OrderInfo orderInfo = cv.getOrders().get(orderId); 52 | assertNotNull(orderInfo); 53 | assertEquals(OrderState.APPROVED, orderInfo.getState()); 54 | return cv; 55 | }); 56 | 57 | assertEquals(creditLimit, customerView.getCreditLimit()); 58 | assertEquals(orderTotal, customerView.getOrders().get(orderId).getOrderTotal()); 59 | 60 | } 61 | 62 | @Test 63 | public void shouldCreateAndRejectOrder() { 64 | 65 | String customerId = createCustomer(creditLimit); 66 | 67 | Money orderTotal = creditLimit.add(new Money(1)); 68 | 69 | String orderId = createOrder(customerId, orderTotal); 70 | 71 | eventually(40, 500, TimeUnit.MILLISECONDS, () -> { 72 | OrderView o = getOrderView(orderId); 73 | assertNotNull(o); 74 | assertEquals(OrderState.REJECTED, o.getState()); 75 | }); 76 | 77 | CustomerView customerView = eventuallyReturning(() -> { 78 | CustomerView cv = getCustomerView(customerId); 79 | assertNotNull(cv); 80 | OrderInfo orderInfo = cv.getOrders().get(orderId); 81 | assertNotNull(orderInfo); 82 | assertEquals(OrderState.REJECTED, orderInfo.getState()); 83 | return cv; 84 | }); 85 | 86 | assertEquals(creditLimit, customerView.getCreditLimit()); 87 | assertEquals(orderTotal, customerView.getOrders().get(orderId).getOrderTotal()); 88 | 89 | } 90 | 91 | @Test 92 | public void shouldRejectOrderWithInvalidCustomerId() { 93 | 94 | Money orderTotal = new Money(720); 95 | 96 | try { 97 | createOrder("unknown-customer-id", orderTotal); 98 | fail(); 99 | } catch (IntegrationTestCustomerNotFoundException e) { 100 | // Expected 101 | } 102 | } 103 | 104 | protected abstract CustomerView getCustomerView(String customerId); 105 | 106 | protected abstract OrderView getOrderView(String orderId); 107 | 108 | protected abstract String createOrder(String customerId, Money orderTotal); 109 | 110 | protected abstract String createCustomer(Money creditLimit); 111 | } 112 | -------------------------------------------------------------------------------- /e2e-test/src/test/java/net/chrisrichardson/eventstore/examples/customersandorders/e2etests/CustomersAndOrdersE2ETest.java: -------------------------------------------------------------------------------- 1 | package net.chrisrichardson.eventstore.examples.customersandorders.e2etests; 2 | 3 | import net.chrisrichardson.eventstore.examples.customersandorders.common.domain.Money; 4 | import net.chrisrichardson.eventstore.examples.customersandorders.commontest.AbstractCustomerAndOrdersIntegrationTest; 5 | import net.chrisrichardson.eventstore.examples.customersandorders.customerscommon.CreateCustomerRequest; 6 | import net.chrisrichardson.eventstore.examples.customersandorders.customerscommon.CreateCustomerResponse; 7 | import net.chrisrichardson.eventstore.examples.customersandorders.orderscommmon.CreateOrderRequest; 8 | import net.chrisrichardson.eventstore.examples.customersandorders.orderscommmon.CreateOrderResponse; 9 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.CustomerView; 10 | import net.chrisrichardson.eventstore.examples.customersandorders.ordershistorycommon.OrderView; 11 | import org.junit.runner.RunWith; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.beans.factory.annotation.Value; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.http.HttpStatus; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 18 | import org.springframework.web.client.HttpClientErrorException; 19 | import org.springframework.web.client.RestTemplate; 20 | 21 | 22 | import static org.junit.Assert.assertEquals; 23 | 24 | @RunWith(SpringJUnit4ClassRunner.class) 25 | @SpringBootTest(classes = CustomersAndOrdersE2ETestConfiguration.class, webEnvironment = SpringBootTest.WebEnvironment.NONE) 26 | public class CustomersAndOrdersE2ETest extends AbstractCustomerAndOrdersIntegrationTest { 27 | 28 | @Value("#{systemEnvironment['DOCKER_HOST_IP']}") 29 | private String hostName; 30 | 31 | private String baseUrlCustomers(String path) { 32 | return "http://"+hostName+":8081/" + path; 33 | } 34 | 35 | private String baseUrlOrders(String path) { 36 | return "http://"+hostName+":8083/" + path; 37 | } 38 | 39 | private String baseUrlOrderHistory(String path) { 40 | return "http://"+hostName+":8082/" + path; 41 | } 42 | 43 | 44 | @Autowired 45 | RestTemplate restTemplate; 46 | 47 | private CustomerView getCustomer(String customerId) { 48 | try { 49 | ResponseEntity getCustomer = 50 | restTemplate.getForEntity(baseUrlOrderHistory("customers/" + customerId), CustomerView.class); 51 | assertEquals(HttpStatus.OK, getCustomer.getStatusCode()); 52 | return getCustomer.getBody(); 53 | } catch (HttpClientErrorException e) { 54 | if (e.getStatusCode() == HttpStatus.NOT_FOUND) 55 | throw new RuntimeException("Cannot find customer "+ customerId, e); 56 | else 57 | throw e; 58 | } 59 | } 60 | 61 | 62 | @Override 63 | protected CustomerView getCustomerView(String customerId) { 64 | return getCustomer(customerId); 65 | } 66 | 67 | @Override 68 | protected OrderView getOrderView(String orderId) { 69 | try { 70 | ResponseEntity getCustomer = 71 | restTemplate.getForEntity(baseUrlOrderHistory("orders/" + orderId), OrderView.class); 72 | assertEquals(HttpStatus.OK, getCustomer.getStatusCode()); 73 | return getCustomer.getBody(); 74 | } catch (HttpClientErrorException e) { 75 | if (e.getStatusCode() == HttpStatus.NOT_FOUND) 76 | throw new RuntimeException("Cannot find order "+ orderId, e); 77 | else 78 | throw e; 79 | } 80 | } 81 | 82 | @Override 83 | protected String createOrder(String customerId, Money orderTotal) { 84 | try { 85 | ResponseEntity orderResponse = 86 | restTemplate.postForEntity(baseUrlOrders("orders"), new CreateOrderRequest(customerId, orderTotal), CreateOrderResponse.class); 87 | assertEquals(HttpStatus.OK, orderResponse.getStatusCode()); 88 | return orderResponse.getBody().getOrderId(); 89 | } catch (HttpClientErrorException e) { 90 | switch (e.getStatusCode()) { 91 | case BAD_REQUEST: 92 | throw new IntegrationTestCustomerNotFoundException(e); 93 | default: 94 | throw e; 95 | } 96 | } 97 | } 98 | 99 | @Override 100 | protected String createCustomer(Money creditLimit) { 101 | ResponseEntity customerResponse = 102 | restTemplate.postForEntity(baseUrlCustomers("customers"), new CreateCustomerRequest("Fred", creditLimit), CreateCustomerResponse.class); 103 | 104 | assertEquals(HttpStatus.OK, customerResponse.getStatusCode()); 105 | CreateCustomerResponse r = customerResponse.getBody(); 106 | return r.getCustomerId(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | --------------------------------------------------------------------------------