├── .circleci ├── build-and-test-arm.sh ├── build-and-test-intel.sh ├── config.yml ├── print-container-ips.sh ├── print-container-logs.sh └── setenv-circle-ci.sh ├── .gitignore ├── .tours └── saga-walkthrough.tour ├── LICENSE.md ├── README.adoc ├── api-gateway-service └── api-gateway-service-main │ ├── Dockerfile │ ├── build.gradle │ └── src │ ├── componentTest │ └── java │ │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ └── apigateway │ │ └── proxies │ │ ├── ApiGatewayComponentTest.java │ │ ├── CustomerServiceProxyTest.java │ │ └── SwaggerUiTest.java │ ├── integrationTest │ └── java │ │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ └── apigateway │ │ └── CustomerServiceProxyTest.java │ └── main │ ├── java │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ └── apigateway │ │ ├── ApiGatewayMain.java │ │ ├── common │ │ └── CommonConfiguration.java │ │ ├── customers │ │ ├── CustomerConfiguration.java │ │ ├── CustomerDestinations.java │ │ ├── GetCustomerHistoryResponse.java │ │ └── OrderHistoryHandlers.java │ │ ├── orders │ │ ├── OrderConfiguration.java │ │ └── OrderDestinations.java │ │ └── proxies │ │ ├── ProxyConfiguration.java │ │ ├── common │ │ └── UnknownProxyException.java │ │ ├── customerservice │ │ ├── CustomerNotFoundException.java │ │ ├── CustomerServiceProxy.java │ │ └── GetCustomerResponse.java │ │ └── orderservice │ │ ├── GetOrderResponse.java │ │ ├── OrderNotFoundException.java │ │ ├── OrderServiceProxy.java │ │ ├── OrderState.java │ │ └── RejectionReason.java │ └── resources │ ├── application.yml │ ├── logback.xml │ └── static │ └── swagger │ └── swagger.yml ├── build-and-test-all-mysql-sharded-outbox.sh ├── build-and-test-all.sh ├── build.gradle ├── buildSrc └── src │ └── main │ └── groovy │ ├── ComponentTestsPlugin.groovy │ ├── EndToEndTestsPlugin.groovy │ ├── IntegrationTestsPlugin.groovy │ └── ServicePlugin.groovy ├── customer-service ├── customer-service-credit-reservation-api │ ├── build.gradle │ └── src │ │ ├── contractTest │ │ ├── java │ │ │ └── io │ │ │ │ └── eventuate │ │ │ │ └── examples │ │ │ │ └── tram │ │ │ │ └── sagas │ │ │ │ └── customersandorders │ │ │ │ └── customers │ │ │ │ └── creditreservationapi │ │ │ │ ├── AbstractMessagingContractTest.java │ │ │ │ └── CommandHandlersTest.java │ │ └── resources │ │ │ ├── application.properties │ │ │ └── contracts │ │ │ └── order-service │ │ │ ├── commands │ │ │ └── reserveCreditCommand.groovy │ │ │ └── replies │ │ │ └── reserveCreditReply.groovy │ │ ├── integrationTest │ │ ├── java │ │ │ └── io │ │ │ │ └── eventuate │ │ │ │ └── examples │ │ │ │ └── tram │ │ │ │ └── sagas │ │ │ │ └── customersandorders │ │ │ │ └── customers │ │ │ │ └── creditreservationapi │ │ │ │ └── CustomerCommandHandlerIntegrationTest.java │ │ └── resources │ │ │ ├── application-postgres.properties │ │ │ └── application.properties │ │ └── main │ │ └── java │ │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ └── customers │ │ └── creditreservationapi │ │ ├── CustomerCommandHandler.java │ │ ├── CustomerCommandHandlerConfiguration.java │ │ ├── CustomerMessagingConfiguration.java │ │ ├── commands │ │ └── ReserveCreditCommand.java │ │ └── replies │ │ ├── CustomerCreditLimitExceeded.java │ │ ├── CustomerCreditReserved.java │ │ ├── CustomerNotFound.java │ │ └── ReserveCreditResult.java ├── customer-service-domain │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ └── customers │ │ └── domain │ │ ├── Customer.java │ │ ├── CustomerCreditLimitExceededException.java │ │ ├── CustomerDomainConfiguration.java │ │ ├── CustomerNotFoundException.java │ │ ├── CustomerRepository.java │ │ └── CustomerService.java ├── customer-service-main │ ├── Dockerfile │ ├── build.gradle │ └── src │ │ ├── componentTest │ │ └── java │ │ │ └── io │ │ │ └── eventuate │ │ │ └── examples │ │ │ └── tram │ │ │ └── sagas │ │ │ └── customersandorders │ │ │ └── customers │ │ │ ├── CustomerServiceComponentTest.java │ │ │ └── CustomerServiceInProcessComponentTest.java │ │ └── main │ │ ├── java │ │ └── io │ │ │ └── eventuate │ │ │ └── examples │ │ │ └── tram │ │ │ └── sagas │ │ │ └── customersandorders │ │ │ └── customers │ │ │ └── CustomerServiceMain.java │ │ └── resources │ │ ├── REMOVE-ME │ │ ├── application-postgres.properties │ │ ├── application.properties │ │ └── logback.xml ├── customer-service-persistence │ ├── build.gradle │ └── src │ │ ├── integrationTest │ │ ├── java │ │ │ └── io │ │ │ │ └── eventuate │ │ │ │ └── examples │ │ │ │ └── tram │ │ │ │ └── sagas │ │ │ │ └── customersandorders │ │ │ │ └── customers │ │ │ │ └── persistence │ │ │ │ └── CustomerServiceRepositoriesTest.java │ │ └── resources │ │ │ ├── application-postgres.properties │ │ │ └── application.properties │ │ └── main │ │ └── java │ │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ └── customers │ │ └── persistence │ │ └── CustomerPersistenceConfiguration.java └── customer-service-restapi │ ├── build.gradle │ └── src │ ├── contractTest │ ├── java │ │ └── io │ │ │ └── eventuate │ │ │ └── examples │ │ │ └── tram │ │ │ └── sagas │ │ │ └── customersandorders │ │ │ └── customers │ │ │ └── restapi │ │ │ └── ApigatewayBase.java │ └── resources │ │ └── contracts │ │ └── apigateway │ │ ├── createCustomer.groovy │ │ └── getCustomer.groovy │ ├── main │ └── java │ │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ └── customers │ │ └── restapi │ │ ├── CreateCustomerRequest.java │ │ ├── CreateCustomerResponse.java │ │ ├── CustomerController.java │ │ ├── CustomerRestApiConfiguration.java │ │ ├── GetCustomerResponse.java │ │ └── GetCustomersResponse.java │ └── test │ └── java │ └── io │ └── eventuate │ └── examples │ └── tram │ └── sagas │ └── customersandorders │ └── customers │ └── restapi │ └── CustomerControllerTest.java ├── end-to-end-tests ├── build.gradle └── src │ └── endToEndTest │ ├── java │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ ├── application │ │ └── CustomersAndOrdersMain.java │ │ └── endtoendtests │ │ ├── ApplicationUnderTest.java │ │ ├── ApplicationUnderTestUsingDockerCompose.java │ │ ├── ApplicationUnderTestUsingKind.java │ │ ├── ApplicationUnderTestUsingStubbed.java │ │ ├── ApplicationUnderTestUsingTestContainers.java │ │ ├── BaseUrlUtils.java │ │ ├── CustomersAndOrdersEndToEndTest.java │ │ ├── CustomersAndOrdersEndToEndTestConfiguration.java │ │ └── proxies │ │ ├── apigateway │ │ └── GetCustomerHistoryResponse.java │ │ ├── customerservice │ │ ├── CreateCustomerRequest.java │ │ ├── CreateCustomerResponse.java │ │ ├── GetCustomerResponse.java │ │ └── GetCustomersResponse.java │ │ └── orderservice │ │ ├── CreateOrderRequest.java │ │ ├── CreateOrderResponse.java │ │ ├── GetOrderResponse.java │ │ ├── GetOrdersResponse.java │ │ ├── OrderState.java │ │ └── RejectionReason.java │ └── resources │ ├── application.properties │ ├── logback.xml │ └── templates │ └── index.html ├── gradle.properties ├── gradle ├── gradlew ├── gradlew.bat └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── Eventuate_Tram_Customer_and_Order_Orchestration_Architecture.png └── Orchestration_flow.jpeg ├── kafka-consumer-groups.sh ├── kafka-topics.sh ├── migration-tests ├── build.gradle └── src │ └── test │ ├── java │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── ordersandcustomers │ │ └── migration │ │ └── DbIdMigrationVerificationTest.java │ └── resources │ ├── application-mssql.properties │ ├── application-postgres.properties │ └── application.properties ├── mise.toml ├── mysql-customer-service-cli.sh ├── mysql-order-service-cli.sh ├── order-service ├── order-service-domain │ ├── build.gradle │ └── src │ │ └── main │ │ └── java │ │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ └── orders │ │ └── domain │ │ ├── Order.java │ │ ├── OrderDetails.java │ │ ├── OrderDomainConfiguration.java │ │ ├── OrderRepository.java │ │ ├── OrderService.java │ │ ├── OrderState.java │ │ └── RejectionReason.java ├── order-service-main │ ├── Dockerfile │ ├── build.gradle │ └── src │ │ ├── componentTest │ │ └── java │ │ │ └── io │ │ │ └── eventuate │ │ │ └── examples │ │ │ └── tram │ │ │ └── sagas │ │ │ └── customersandorders │ │ │ └── orders │ │ │ └── OrderServiceInProcessComponentTest.java │ │ └── main │ │ ├── java │ │ └── io │ │ │ └── eventuate │ │ │ └── examples │ │ │ └── tram │ │ │ └── sagas │ │ │ └── customersandorders │ │ │ └── orders │ │ │ └── OrderServiceMain.java │ │ └── resources │ │ ├── application-postgres.properties │ │ └── application.properties ├── order-service-persistence │ ├── build.gradle │ └── src │ │ ├── integrationTest │ │ ├── java │ │ │ └── io │ │ │ │ └── eventuate │ │ │ │ └── examples │ │ │ │ └── tram │ │ │ │ └── sagas │ │ │ │ └── customersandorders │ │ │ │ └── orders │ │ │ │ └── persistence │ │ │ │ └── OrderServiceRepositoriesTest.java │ │ └── resources │ │ │ ├── application-postgres.properties │ │ │ └── application.properties │ │ └── main │ │ └── java │ │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ └── orders │ │ └── persistence │ │ └── OrderPersistenceConfiguration.java ├── order-service-proxies-customer-service │ ├── build.gradle │ └── src │ │ ├── contractTest │ │ ├── java │ │ │ └── io │ │ │ │ └── eventuate │ │ │ │ └── examples │ │ │ │ └── tram │ │ │ │ └── sagas │ │ │ │ └── customersandorders │ │ │ │ └── orders │ │ │ │ └── proxies │ │ │ │ └── customers │ │ │ │ ├── BaseForCustomerServiceTest.java │ │ │ │ ├── MoneyModule.java │ │ │ │ ├── ReplyHandlersTest.java │ │ │ │ └── TestReplyConsumer.java │ │ └── resources │ │ │ └── application.properties │ │ └── main │ │ └── java │ │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ ├── customers │ │ └── creditreservationapi │ │ │ ├── commands │ │ │ └── ReserveCreditCommand.java │ │ │ └── replies │ │ │ ├── CustomerCreditLimitExceeded.java │ │ │ ├── CustomerCreditReserved.java │ │ │ ├── CustomerNotFound.java │ │ │ └── ReserveCreditResult.java │ │ └── orders │ │ └── proxies │ │ └── customers │ │ ├── CustomerServiceProxy.java │ │ └── CustomerServiceProxyConfiguration.java ├── order-service-restapi │ ├── build.gradle │ └── src │ │ ├── main │ │ └── java │ │ │ └── io │ │ │ └── eventuate │ │ │ └── examples │ │ │ └── tram │ │ │ └── sagas │ │ │ └── customersandorders │ │ │ └── orders │ │ │ └── restapi │ │ │ ├── CreateOrderRequest.java │ │ │ ├── CreateOrderResponse.java │ │ │ ├── GetOrderResponse.java │ │ │ ├── GetOrdersResponse.java │ │ │ ├── OrderController.java │ │ │ └── OrderRestApiConfiguration.java │ │ └── test │ │ └── java │ │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── ordersandorders │ │ └── orders │ │ └── web │ │ └── OrderControllerTest.java └── order-service-sagas │ ├── build.gradle │ └── src │ ├── main │ └── java │ │ └── io │ │ └── eventuate │ │ └── examples │ │ └── tram │ │ └── sagas │ │ └── customersandorders │ │ └── orders │ │ └── sagas │ │ ├── CreateOrderSaga.java │ │ ├── CreateOrderSagaData.java │ │ ├── OrderSagaService.java │ │ └── OrderSagasConfiguration.java │ └── test │ └── java │ └── io │ └── eventuate │ └── examples │ └── tram │ └── sagas │ └── customersandorders │ └── orders │ └── service │ └── CreateOrderSagaTest.java ├── postgres-customer-service-cli.sh ├── postgres-order-service-cli.sh ├── settings.gradle ├── test-curl-commands.sh ├── wait-for-mysql.sh ├── wait-for-postgres.sh └── wait-for-services.sh /.circleci/build-and-test-arm.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | # See https://github.com/testcontainers/testcontainers-java/issues/5524 4 | 5 | docker run --privileged --rm tonistiigi/binfmt --install amd64 6 | 7 | ./build-and-test-all.sh -------------------------------------------------------------------------------- /.circleci/build-and-test-intel.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | ./build-and-test-all.sh -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | eventuate-gradle-build-and-test: "eventuate_io/eventuate-gradle-build-and-test@0.2.9" 4 | workflows: 5 | version: 2.1 6 | build-test-and-deploy: 7 | jobs: 8 | - eventuate-gradle-build-and-test/build-and-test: 9 | name: mysql-intel 10 | resource_class: large 11 | java_version_to_install: "17" 12 | script: ./.circleci/build-and-test-intel.sh 13 | - eventuate-gradle-build-and-test/build-and-test: 14 | name: mysql-arm 15 | resource_class: arm.large 16 | java_version_to_install: "17" 17 | script: ./.circleci/build-and-test-arm.sh 18 | # - eventuate-gradle-build-and-test/build-and-test: 19 | # name: mysql-sharded-outbox 20 | # script: ./build-and-test-all-mysql-sharded-outbox.sh 21 | -------------------------------------------------------------------------------- /.circleci/print-container-ips.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | docker ps -a | cut -f1 -d\ | tail +2 | xargs -n1 docker inspect -f '{{.Name}} {{.ID}} {{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' -------------------------------------------------------------------------------- /.circleci/print-container-logs.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | CONTAINER_IDS=$(docker ps -a -q) 4 | 5 | for id in $CONTAINER_IDS ; do 6 | echo "\n--------------------" 7 | echo "logs of:\n" 8 | docker ps -a -f "id=$id" 9 | echo "\n" 10 | docker logs $id || echo docker logs failed for $id 11 | echo "--------------------\n" 12 | done 13 | 14 | mkdir -p ~/container-logs 15 | 16 | docker ps -a > ~/container-logs/containers.txt 17 | 18 | for name in $(docker ps -a --format "{{.Names}}") ; do 19 | echo Getting log for $name 20 | (docker logs $name || echo docker logs failed for $name) > ~/container-logs/${name}.log 21 | done 22 | -------------------------------------------------------------------------------- /.circleci/setenv-circle-ci.sh: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Host DNS name doesn't resolve in Docker alpine images 4 | 5 | export TERM=dumb 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | *.idea/ 4 | *.iml 5 | *.log 6 | tmp-migration 7 | 8 | out 9 | .DS_Store 10 | bin 11 | .vscode 12 | -------------------------------------------------------------------------------- /.tours/saga-walkthrough.tour: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://aka.ms/codetour-schema", 3 | "title": "saga-walkthrough", 4 | "steps": [ 5 | { 6 | "file": "order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/ordersandcustomers/orders/sagas/CreateOrderSaga.java", 7 | "description": "Here's the heart of the `CreateOrderSaga`, which defines the saga's steps.", 8 | "line": 24 9 | }, 10 | { 11 | "file": "order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/ordersandcustomers/orders/sagas/OrderSagaService.java", 12 | "description": "In the `Order Service` the `createOrder()` method instantiates the saga.", 13 | "line": 25 14 | }, 15 | { 16 | "file": "order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/ordersandcustomers/orders/sagas/CreateOrderSaga.java", 17 | "description": "The first step of the saga executes locally within the `Order Service`.", 18 | "line": 26 19 | }, 20 | { 21 | "file": "order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/ordersandcustomers/orders/sagas/CreateOrderSaga.java", 22 | "description": "The `create()` method creates and persists an `Order`. The `order ID` is saved in the Saga data", 23 | "line": 50 24 | }, 25 | { 26 | "file": "order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/ordersandcustomers/orders/sagas/CreateOrderSaga.java", 27 | "description": "The second step of the saga is then executed. It invokes the `reserveCredit()` method, which returns the message to send to the `Customer Service`", 28 | "line": 29 29 | }, 30 | { 31 | "file": "order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/ordersandcustomers/orders/sagas/CreateOrderSaga.java", 32 | "description": "This method returns the `CommandWithDestination`, which contains a message and the destination channel.", 33 | "line": 55 34 | }, 35 | { 36 | "file": "order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/ordersandcustomers/orders/sagas/OrderSagaService.java", 37 | "description": "The `createOrder()` method retrieves and returns the `Order`. Since the service uses the Transaction Outbox pattern, when this method returns, the message to send to the `Customer Service` to reserve credit is committed to the database.", 38 | "line": 28 39 | }, 40 | { 41 | "file": "docker-compose-mysql.yml", 42 | "description": "The `CDC service` retrieves the message from the outbox table and sends it to Apache Kafka", 43 | "line": 122 44 | }, 45 | { 46 | "file": "customer-service/customer-service-messaging/src/main/java/io/eventuate/examples/tram/sagas/ordersandcustomers/customers/messaging/CustomerCommandHandler.java", 47 | "description": "Here's how the `Customer Service` configures a command handler for the `ReserveCreditCommand`", 48 | "line": 29 49 | }, 50 | { 51 | "file": "customer-service/customer-service-messaging/src/main/java/io/eventuate/examples/tram/sagas/ordersandcustomers/customers/messaging/CustomerCommandHandler.java", 52 | "description": "On the happy path, the `reserveCredit()` method replies with a success reply indicating the credit was reserved", 53 | "line": 37 54 | }, 55 | { 56 | "file": "order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/ordersandcustomers/orders/sagas/CreateOrderSaga.java", 57 | "description": "Back in the `Order Service`: if the credit was successfully reserved (as indicated by a success reply), the third step of the saga is executed. It invokes the local `approve()` method.", 58 | "line": 33 59 | }, 60 | { 61 | "file": "order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/ordersandcustomers/orders/sagas/CreateOrderSaga.java", 62 | "description": "The `approve()` method approves the `Order`.", 63 | "line": 63 64 | } 65 | ] 66 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 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 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG baseImageVersion 2 | FROM eventuateio/eventuate-examples-docker-images-spring-example-base-image:$baseImageVersion 3 | ARG serviceImageVersion 4 | COPY build/libs/api-gateway-service-main-$serviceImageVersion.jar service.jar 5 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | apply plugin: "java" 3 | apply plugin: 'org.springframework.boot' 4 | apply plugin: IntegrationTestsPlugin 5 | apply plugin: ComponentTestsPlugin 6 | 7 | configurations.all { 8 | exclude module: "spring-boot-starter-web" 9 | } 10 | 11 | dependencies { 12 | implementation "io.eventuate.examples.common:eventuate-examples-common-money-jakarta9:$eventuateCommonExamplesVersion" 13 | 14 | implementation "io.eventuate.util:eventuate-util-swagger-ui" 15 | 16 | implementation "io.projectreactor:reactor-tools" 17 | 18 | implementation "org.springframework.boot:spring-boot-starter-webflux" 19 | implementation "org.springframework.cloud:spring-cloud-starter-gateway" 20 | 21 | implementation "org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j" 22 | 23 | implementation "org.springframework.boot:spring-boot-starter-actuator" 24 | 25 | implementation "org.springframework.boot:spring-boot-starter-web:3.0.13" 26 | implementation "io.micrometer:micrometer-tracing-bridge-brave" 27 | implementation "io.zipkin.reporter2:zipkin-reporter-brave" 28 | 29 | testImplementation "org.springframework.cloud:spring-cloud-contract-wiremock" 30 | testImplementation "org.springframework.boot:spring-boot-starter-test" 31 | 32 | testImplementation "org.springframework.cloud:spring-cloud-contract-wiremock" 33 | testImplementation "org.springframework.cloud:spring-cloud-starter-contract-stub-runner" 34 | 35 | implementation group: 'io.netty', name: 'netty-resolver-dns-native-macos', version: '4.1.75.Final', classifier: 'osx-aarch_64' 36 | 37 | } 38 | 39 | integrationTest { 40 | systemProperty "stubrunner.repositoryRoot", contractRepoUrl 41 | dependsOn(":customer-service:customer-service-restapi:publishStubsPublicationToStubsRepository") 42 | } 43 | 44 | 45 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/componentTest/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/ApiGatewayComponentTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.github.tomakehurst.wiremock.stubbing.StubMapping; 6 | import org.json.JSONArray; 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 12 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.boot.test.web.server.LocalServerPort; 15 | import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; 16 | import org.springframework.context.annotation.Configuration; 17 | import org.springframework.context.annotation.Import; 18 | import org.springframework.web.reactive.function.client.WebClient; 19 | 20 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 21 | import static org.junit.jupiter.api.Assertions.assertEquals; 22 | 23 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 24 | properties = {"customer.destinations.customerServiceUrl=http://localhost:${wiremock.server.port}", "order.destinations.orderServiceUrl=http://localhost:${wiremock.server.port}"}) 25 | @AutoConfigureWireMock(port = 0) 26 | public class ApiGatewayComponentTest { 27 | 28 | @Configuration 29 | @EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class) 30 | @Import(ProxyConfiguration.class) 31 | static public class Config { 32 | } 33 | 34 | @LocalServerPort 35 | private long port; 36 | 37 | @Autowired 38 | private WebClient webClient; 39 | 40 | 41 | @Test 42 | public void shouldGetOrder() { 43 | 44 | var expectedResponse = "hello"; 45 | 46 | StubMapping stubMapping = stubFor(get(urlEqualTo("/orders/101")) 47 | .willReturn(aResponse() 48 | .withStatus(200) 49 | .withHeader("Content-Type", "application/json") 50 | .withBody(expectedResponse))); 51 | 52 | String response = webClient.get().uri("http://localhost:" + port + "/orders/101").retrieve().bodyToMono(String.class).block(); 53 | 54 | assertEquals(expectedResponse, response); 55 | 56 | verify(getRequestedFor(urlMatching("/orders/101"))); 57 | } 58 | 59 | @Test 60 | public void shouldGetOrders() { 61 | 62 | var expectedResponse = "hello-every-order"; 63 | 64 | StubMapping stubMapping = stubFor(get(urlEqualTo("/orders")) 65 | .willReturn(aResponse() 66 | .withStatus(200) 67 | .withHeader("Content-Type", "application/json") 68 | .withBody(expectedResponse))); 69 | 70 | String response = webClient.get().uri("http://localhost:" + port + "/orders").retrieve().bodyToMono(String.class).block(); 71 | 72 | assertEquals(expectedResponse, response); 73 | 74 | verify(getRequestedFor(urlMatching("/orders"))); 75 | } 76 | 77 | @Test 78 | public void shouldGetOrderHistory() throws JSONException, JsonProcessingException { 79 | 80 | var customerJSon = new JSONObject(); 81 | customerJSon.put("customerId", 101); 82 | customerJSon.put("name", "Fred"); 83 | customerJSon.put("creditLimit", "12.34"); 84 | 85 | var ordersJSon = new JSONArray(); 86 | ordersJSon.put(new JSONObject().put("orderId", 1).put("orderState", "APPROVED").put("rejectionReason", JSONObject.NULL)); 87 | 88 | 89 | stubFor(get(urlEqualTo("/orders/customer/5")) 90 | .willReturn(aResponse() 91 | .withStatus(200) 92 | .withHeader("Content-Type", "application/json") 93 | .withBody(ordersJSon.toString()))); 94 | 95 | stubFor(get(urlEqualTo("/customers/5")) 96 | .willReturn(aResponse() 97 | .withStatus(200) 98 | .withHeader("Content-Type", "application/json") 99 | .withBody(customerJSon.toString()))); 100 | 101 | var mapper = new ObjectMapper(); 102 | 103 | var response = webClient.get().uri("http://localhost:" + port + "/customers/5/orderhistory").retrieve().bodyToMono(String.class).block(); 104 | 105 | customerJSon.put("orders", ordersJSon); 106 | customerJSon.put("creditLimit", new JSONObject().put("amount", 12.34)); 107 | assertEquals(mapper.readTree(customerJSon.toString()), mapper.readTree(response)); 108 | 109 | verify(getRequestedFor(urlMatching("/customers/5"))); 110 | verify(getRequestedFor(urlMatching("/orders/customer/5"))); 111 | } 112 | 113 | } -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/componentTest/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/CustomerServiceProxyTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.common.UnknownProxyException; 4 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.customerservice.CustomerServiceProxy; 5 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.customerservice.GetCustomerResponse; 6 | import io.github.resilience4j.circuitbreaker.CallNotPermittedException; 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 12 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.boot.test.web.server.LocalServerPort; 15 | import org.springframework.cloud.contract.wiremock.AutoConfigureWireMock; 16 | import org.springframework.context.annotation.Configuration; 17 | import org.springframework.context.annotation.Import; 18 | import org.springframework.web.reactive.function.client.WebClient; 19 | 20 | import java.util.stream.IntStream; 21 | 22 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 23 | import static org.junit.jupiter.api.Assertions.assertEquals; 24 | import static org.junit.jupiter.api.Assertions.assertThrows; 25 | 26 | @SpringBootTest(classes = CustomerServiceProxyTest.Config.class, 27 | webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, 28 | properties = {"customer.destinations.customerServiceUrl=http://localhost:${wiremock.server.port}", "order.destinations.orderServiceUrl=http://localhost:${wiremock.server.port}"}) 29 | @AutoConfigureWireMock(port = 0) 30 | public class CustomerServiceProxyTest { 31 | 32 | @Configuration 33 | @EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class) 34 | @Import(ProxyConfiguration.class) 35 | static public class Config { 36 | } 37 | 38 | @Autowired 39 | private CustomerServiceProxy customerServiceProxy; 40 | 41 | @LocalServerPort 42 | private long port; 43 | 44 | @Autowired 45 | private WebClient webClient; 46 | 47 | @Test 48 | public void shouldCallCustomerService() throws JSONException { 49 | 50 | JSONObject json = new JSONObject(); 51 | json.put("customerId", 101); 52 | json.put("name", "Fred"); 53 | json.put("creditLimit", "12.34"); 54 | 55 | String expectedResponse = json.toString(); 56 | 57 | stubFor(get(urlEqualTo("/customers/101")) 58 | .willReturn(aResponse() 59 | .withStatus(200) 60 | .withHeader("Content-Type", "application/json") 61 | .withBody(expectedResponse))); 62 | 63 | GetCustomerResponse customer = customerServiceProxy.findCustomerById("101").block().get(); 64 | 65 | assertEquals(Long.valueOf(101L), customer.customerId()); 66 | 67 | verify(getRequestedFor(urlMatching("/customers/101"))); 68 | } 69 | 70 | @Test 71 | public void shouldTimeoutAndTripCircuitBreaker() { 72 | assertThrows(CallNotPermittedException.class, () -> { 73 | 74 | String expectedResponse = "{}"; 75 | 76 | stubFor(get(urlEqualTo("/customers/99")) 77 | .willReturn(aResponse() 78 | .withStatus(500) 79 | .withHeader("Content-Type", "application/json") 80 | .withBody(expectedResponse))); 81 | 82 | 83 | IntStream.range(0, 100).forEach(i -> { 84 | try { 85 | customerServiceProxy.findCustomerById("99").block(); 86 | } catch (CallNotPermittedException e) { 87 | throw e; 88 | } catch (UnknownProxyException e) { 89 | // 90 | } 91 | } 92 | ); 93 | 94 | verify(getRequestedFor(urlMatching("/customers/99"))); 95 | }); 96 | } 97 | } -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/componentTest/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/SwaggerUiTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 6 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.boot.test.web.server.LocalServerPort; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.Import; 11 | 12 | import java.io.IOException; 13 | import java.net.HttpURLConnection; 14 | import java.net.URL; 15 | 16 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 17 | public class SwaggerUiTest { 18 | 19 | @LocalServerPort 20 | int port; 21 | 22 | private String hostName = "localhost"; 23 | 24 | @Configuration 25 | @EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class) 26 | @Import(ProxyConfiguration.class) 27 | static public class Config { 28 | } 29 | 30 | @Test 31 | public void testSwaggerUiUrls() throws IOException { 32 | testSwaggerUiUrl(port); 33 | } 34 | 35 | @Test 36 | public void testSwaggerYml() throws IOException { 37 | assertUrlStatusIsOk("http://%s:%s/swagger/swagger.yml".formatted(hostName, port)); 38 | } 39 | 40 | private void testSwaggerUiUrl(int port) throws IOException { 41 | assertUrlStatusIsOk("http://%s:%s/swagger-ui/index.html".formatted(hostName, port)); 42 | } 43 | 44 | private void assertUrlStatusIsOk(String url) throws IOException { 45 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); 46 | if (connection.getResponseCode() != 200) 47 | Assertions.fail("Expected 200 for %s, got %s".formatted(url, connection.getResponseCode())); 48 | } 49 | 50 | 51 | } -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/integrationTest/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/CustomerServiceProxyTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.customerservice.CustomerServiceProxy; 5 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.customerservice.GetCustomerResponse; 6 | import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; 7 | import io.github.resilience4j.timelimiter.TimeLimiterRegistry; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; 13 | import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.test.annotation.DirtiesContext; 16 | import org.springframework.web.reactive.function.client.WebClient; 17 | 18 | import static org.junit.jupiter.api.Assertions.assertEquals; 19 | 20 | @SpringBootTest(webEnvironment= SpringBootTest.WebEnvironment.NONE) 21 | @AutoConfigureStubRunner(ids = {"io.eventuate.examples.tram.sagas.customersandorders:customer-service-restapi:+"}, 22 | stubsMode = StubRunnerProperties.StubsMode.REMOTE) 23 | @DirtiesContext 24 | public class CustomerServiceProxyTest { 25 | 26 | @Value("${stubrunner.runningstubs.customer-service-restapi.port}") 27 | private int port; 28 | 29 | @Configuration 30 | static public class Config { 31 | } 32 | 33 | private CustomerServiceProxy customerServiceProxy; 34 | 35 | @BeforeEach 36 | public void setUp() { 37 | customerServiceProxy = new CustomerServiceProxy(WebClient.builder().build(), 38 | CircuitBreakerRegistry.custom().build(), "http://localhost:%s".formatted(port), TimeLimiterRegistry.ofDefaults()); 39 | } 40 | @Test 41 | public void shouldCallCustomerService() { 42 | GetCustomerResponse customer = customerServiceProxy.findCustomerById("101").block().get(); 43 | 44 | assertEquals(Long.valueOf(101L), customer.customerId()); 45 | assertEquals("Chris", customer.name()); 46 | assertEquals(new Money("123.45"), customer.creditLimit()); 47 | 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/ApiGatewayMain.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway; 2 | 3 | 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 7 | import reactor.tools.agent.ReactorDebugAgent; 8 | 9 | @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) 10 | public class ApiGatewayMain { 11 | 12 | public static void main(String[] args) { 13 | ReactorDebugAgent.init(); 14 | SpringApplication.run(ApiGatewayMain.class, args); 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/common/CommonConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.common; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.core.io.ClassPathResource; 6 | import org.springframework.web.reactive.function.client.WebClient; 7 | import org.springframework.web.reactive.function.server.RouterFunction; 8 | import org.springframework.web.reactive.function.server.RouterFunctions; 9 | import org.springframework.web.reactive.function.server.ServerResponse; 10 | 11 | @Configuration 12 | public class CommonConfiguration { 13 | @Bean 14 | public WebClient webClient() { 15 | return WebClient.create(); 16 | } 17 | 18 | @Bean 19 | public RouterFunction swaggerRouter1() { 20 | return RouterFunctions.resources("/swagger/**", new ClassPathResource("static/swagger/")); 21 | } 22 | 23 | @Bean 24 | public RouterFunction swaggerRouter2() { 25 | return RouterFunctions.resources("/swagger-ui/**", new ClassPathResource("META-INF/static-content/swagger-ui/")); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/customers/CustomerConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.customers; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.customerservice.CustomerServiceProxy; 4 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.orderservice.OrderServiceProxy; 5 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 6 | import org.springframework.cloud.gateway.route.RouteLocator; 7 | import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.web.reactive.function.server.RouterFunction; 11 | import org.springframework.web.reactive.function.server.RouterFunctions; 12 | import org.springframework.web.reactive.function.server.ServerResponse; 13 | 14 | import static org.springframework.web.reactive.function.server.RequestPredicates.GET; 15 | 16 | @Configuration 17 | @EnableConfigurationProperties(CustomerDestinations.class) 18 | public class CustomerConfiguration { 19 | 20 | @Bean 21 | public RouterFunction orderHistoryHandlerRouting(OrderHistoryHandlers orderHistoryHandlers) { 22 | return RouterFunctions.route(GET("/customers/{customerId}/orderhistory"), orderHistoryHandlers::getOrderHistory); 23 | } 24 | 25 | @Bean 26 | public OrderHistoryHandlers orderHistoryHandlers(OrderServiceProxy orderService, CustomerServiceProxy customerService) { 27 | return new OrderHistoryHandlers(orderService, customerService); 28 | } 29 | 30 | @Bean 31 | public RouteLocator customerProxyRouting(RouteLocatorBuilder builder, CustomerDestinations customerDestinations) { 32 | return builder.routes() 33 | .route(r -> r.path("/customers/**").and().method("GET").uri(customerDestinations.getCustomerServiceUrl())) 34 | .build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/customers/CustomerDestinations.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.customers; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | import jakarta.validation.constraints.NotNull; 6 | 7 | @ConfigurationProperties(prefix = "customer.destinations") 8 | public class CustomerDestinations { 9 | 10 | @NotNull 11 | private String customerServiceUrl; 12 | 13 | public String getCustomerServiceUrl() { 14 | return customerServiceUrl; 15 | } 16 | 17 | public void setCustomerServiceUrl(String customerServiceUrl) { 18 | this.customerServiceUrl = customerServiceUrl; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/customers/GetCustomerHistoryResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.customers; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.orderservice.GetOrderResponse; 5 | 6 | import java.util.List; 7 | 8 | public record GetCustomerHistoryResponse(Long customerId, String name, Money creditLimit, List orders) { 9 | public Money getCreditLimit() { 10 | return creditLimit(); 11 | } 12 | 13 | public Long getCustomerId() { 14 | return customerId(); 15 | } 16 | 17 | public String getName() { 18 | return name(); 19 | } 20 | 21 | public List getOrders() { 22 | return orders(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/customers/OrderHistoryHandlers.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.customers; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.customerservice.CustomerServiceProxy; 4 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.customerservice.GetCustomerResponse; 5 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.orderservice.GetOrderResponse; 6 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.orderservice.OrderServiceProxy; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.web.reactive.function.server.ServerRequest; 9 | import org.springframework.web.reactive.function.server.ServerResponse; 10 | import reactor.core.publisher.Mono; 11 | 12 | import java.util.List; 13 | import java.util.Optional; 14 | 15 | import static org.springframework.web.reactive.function.BodyInserters.fromValue; 16 | 17 | public class OrderHistoryHandlers { 18 | 19 | private final OrderServiceProxy orderService; 20 | private final CustomerServiceProxy customerService; 21 | 22 | public OrderHistoryHandlers(OrderServiceProxy orderService, CustomerServiceProxy customerService) { 23 | this.orderService = orderService; 24 | this.customerService = customerService; 25 | } 26 | 27 | public Mono getOrderHistory(ServerRequest serverRequest) { 28 | String customerId = serverRequest.pathVariable("customerId"); 29 | 30 | Mono> customer = customerService.findCustomerById(customerId); 31 | 32 | Mono> orders = orderService.findOrdersByCustomerId(customerId); 33 | 34 | Mono> map = Mono 35 | .zip(customer, orders) 36 | .map(possibleCustomerAndOrders -> 37 | possibleCustomerAndOrders.getT1().map(c -> { 38 | List os = possibleCustomerAndOrders.getT2(); 39 | return new GetCustomerHistoryResponse(c.customerId(), c.name(), c.creditLimit(), os); 40 | })); 41 | return map.flatMap(maybe -> 42 | maybe.map(c -> 43 | ServerResponse.ok() 44 | .contentType(MediaType.APPLICATION_JSON) 45 | .body(fromValue(c))) 46 | .orElseGet(() -> ServerResponse.notFound().build())); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/orders/OrderConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.orders; 2 | 3 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 4 | import org.springframework.cloud.gateway.route.RouteLocator; 5 | import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | @EnableConfigurationProperties(OrderDestinations.class) 11 | public class OrderConfiguration { 12 | @Bean 13 | public RouteLocator orderProxyRouting(RouteLocatorBuilder builder, OrderDestinations orderDestinations) { 14 | return builder.routes() 15 | .route(r -> r.path("/orders/customer/**").and().method("GET").uri(orderDestinations.getOrderServiceUrl())) 16 | .build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/orders/OrderDestinations.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.orders; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | import jakarta.validation.constraints.NotNull; 6 | 7 | @ConfigurationProperties(prefix = "order.destinations") 8 | public class OrderDestinations { 9 | 10 | @NotNull 11 | private String orderServiceUrl; 12 | 13 | public String getOrderServiceUrl() { 14 | return orderServiceUrl; 15 | } 16 | 17 | public void setOrderServiceUrl(String orderServiceUrl) { 18 | this.orderServiceUrl = orderServiceUrl; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/ProxyConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.common.CommonConfiguration; 4 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.customers.CustomerConfiguration; 5 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.customers.CustomerDestinations; 6 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.orders.OrderConfiguration; 7 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.orders.OrderDestinations; 8 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.customerservice.CustomerServiceProxy; 9 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.orderservice.OrderServiceProxy; 10 | import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; 11 | import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; 12 | import io.github.resilience4j.timelimiter.TimeLimiterConfig; 13 | import io.github.resilience4j.timelimiter.TimeLimiterRegistry; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | import org.springframework.beans.factory.annotation.Value; 17 | import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; 18 | import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder; 19 | import org.springframework.cloud.client.circuitbreaker.Customizer; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.context.annotation.Configuration; 22 | import org.springframework.context.annotation.Import; 23 | import org.springframework.web.reactive.function.client.WebClient; 24 | 25 | import java.time.Duration; 26 | 27 | @Configuration 28 | @Import({CommonConfiguration.class, OrderConfiguration.class, CustomerConfiguration.class}) 29 | public class ProxyConfiguration { 30 | 31 | private Logger logger = LoggerFactory.getLogger(getClass()); 32 | 33 | @Value("${apigateway.timeout.millis}") 34 | private long apiGatewayTimeoutMillis; 35 | 36 | @Bean 37 | public OrderServiceProxy orderServiceProxy(OrderDestinations orderDestinations, WebClient client, CircuitBreakerRegistry circuitBreakerRegistry, TimeLimiterRegistry timeLimiterRegistry) { 38 | return new OrderServiceProxy(orderDestinations, client, circuitBreakerRegistry, timeLimiterRegistry); 39 | } 40 | 41 | @Bean 42 | public CustomerServiceProxy customerServiceProxy(CustomerDestinations customerDestinations, WebClient client, CircuitBreakerRegistry circuitBreakerRegistry, TimeLimiterRegistry timeLimiterRegistry) { 43 | return new CustomerServiceProxy(client, circuitBreakerRegistry, customerDestinations.getCustomerServiceUrl(), timeLimiterRegistry); 44 | } 45 | 46 | @Bean 47 | public TimeLimiterRegistry timeLimiterRegistry() { 48 | logger.info("apiGatewayTimeoutMillis={}", apiGatewayTimeoutMillis); 49 | return TimeLimiterRegistry.of(TimeLimiterConfig.custom() 50 | .timeoutDuration(Duration.ofMillis(apiGatewayTimeoutMillis)).build()); 51 | } 52 | 53 | @Bean 54 | public Customizer defaultCustomizer() { 55 | return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) 56 | .circuitBreakerConfig(CircuitBreakerConfig.ofDefaults()) 57 | .timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofMillis(apiGatewayTimeoutMillis)).build()).build()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/common/UnknownProxyException.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.common; 2 | 3 | import org.springframework.http.HttpStatusCode; 4 | 5 | public class UnknownProxyException extends RuntimeException{ 6 | public UnknownProxyException(String message) { 7 | super(message); 8 | } 9 | 10 | public static UnknownProxyException make(String path, HttpStatusCode statusCode, String param) { 11 | return new UnknownProxyException("Unknown: " + path + param + "=" + statusCode); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/customerservice/CustomerNotFoundException.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.customerservice; 2 | 3 | public class CustomerNotFoundException extends RuntimeException { 4 | public CustomerNotFoundException() { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/customerservice/CustomerServiceProxy.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.customerservice; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.common.UnknownProxyException; 4 | import io.github.resilience4j.circuitbreaker.CircuitBreaker; 5 | import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; 6 | import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator; 7 | import io.github.resilience4j.reactor.timelimiter.TimeLimiterOperator; 8 | import io.github.resilience4j.timelimiter.TimeLimiter; 9 | import io.github.resilience4j.timelimiter.TimeLimiterRegistry; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.web.reactive.function.client.ClientResponse; 12 | import org.springframework.web.reactive.function.client.WebClient; 13 | import reactor.core.publisher.Mono; 14 | 15 | import java.util.Optional; 16 | 17 | public class CustomerServiceProxy { 18 | private final CircuitBreaker cb; 19 | 20 | private WebClient client; 21 | private String customerServiceUrl; 22 | private TimeLimiter timeLimiter; 23 | 24 | public CustomerServiceProxy(WebClient client, CircuitBreakerRegistry circuitBreakerRegistry, String customerServiceUrl, TimeLimiterRegistry timeLimiterRegistry) { 25 | this.client = client; 26 | this.cb = circuitBreakerRegistry.circuitBreaker("CUSTOMER_SERVICE_CIRCUIT_BREAKER"); 27 | this.timeLimiter = timeLimiterRegistry.timeLimiter("CUSTOMER_SERVICE_TIME_LIMITER"); 28 | this.customerServiceUrl = customerServiceUrl; 29 | } 30 | 31 | public Mono> findCustomerById(String customerId) { 32 | Mono response = client 33 | .get() 34 | .uri(customerServiceUrl + "/customers/{customerId}", customerId) 35 | .exchange(); 36 | return response.flatMap(resp -> { 37 | if (resp.statusCode().value() == HttpStatus.OK.value()) 38 | return resp.bodyToMono(GetCustomerResponse.class).map(Optional::of); 39 | else if (resp.statusCode().value() == HttpStatus.NOT_FOUND.value()) { 40 | Mono> notFound = Mono.just(Optional.empty()); 41 | return notFound; 42 | } else 43 | return Mono.error(UnknownProxyException.make("/customers/", resp.statusCode(), customerId)); 44 | }) 45 | .transformDeferred(TimeLimiterOperator.of(timeLimiter)) 46 | .transformDeferred(CircuitBreakerOperator.of(cb)) 47 | //.onErrorResume(CallNotPermittedException.class, e -> Mono.just(null)) 48 | ; 49 | } 50 | 51 | 52 | } 53 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/customerservice/GetCustomerResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.customerservice; 2 | 3 | 4 | import io.eventuate.examples.common.money.Money; 5 | 6 | public record GetCustomerResponse(Long customerId, String name, Money creditLimit) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/orderservice/GetOrderResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.orderservice; 2 | 3 | public record GetOrderResponse(Long orderId, OrderState orderState, RejectionReason rejectionReason) { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/orderservice/OrderNotFoundException.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.orderservice; 2 | 3 | public class OrderNotFoundException extends RuntimeException { 4 | public OrderNotFoundException() { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/orderservice/OrderServiceProxy.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.orderservice; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.orders.OrderDestinations; 4 | import io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.common.UnknownProxyException; 5 | import io.github.resilience4j.circuitbreaker.CircuitBreaker; 6 | import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; 7 | import io.github.resilience4j.reactor.circuitbreaker.operator.CircuitBreakerOperator; 8 | import io.github.resilience4j.reactor.timelimiter.TimeLimiterOperator; 9 | import io.github.resilience4j.timelimiter.TimeLimiter; 10 | import io.github.resilience4j.timelimiter.TimeLimiterRegistry; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.web.reactive.function.client.WebClient; 13 | import reactor.core.publisher.Mono; 14 | 15 | import java.util.Arrays; 16 | import java.util.List; 17 | 18 | public class OrderServiceProxy { 19 | private final CircuitBreaker circuitBreaker; 20 | private final TimeLimiter timeLimiter; 21 | private final OrderDestinations orderDestinations; 22 | 23 | private final WebClient client; 24 | 25 | public OrderServiceProxy(OrderDestinations orderDestinations, WebClient client, CircuitBreakerRegistry circuitBreakerRegistry, TimeLimiterRegistry timeLimiterRegistry) { 26 | this.orderDestinations = orderDestinations; 27 | this.client = client; 28 | this.circuitBreaker = circuitBreakerRegistry.circuitBreaker("ORDER_SERVICE_CIRCUIT_BREAKER"); 29 | this.timeLimiter = timeLimiterRegistry.timeLimiter("ORDER_SERVICE_CIRCUIT_BREAKER"); 30 | } 31 | 32 | public Mono> findOrdersByCustomerId(String customerId) { 33 | return client 34 | .get() 35 | .uri(orderDestinations.getOrderServiceUrl() + "/orders/customer/{customerId}", customerId) 36 | .retrieve() 37 | .onStatus(status -> status != HttpStatus.OK, response -> Mono.error(UnknownProxyException.make("/orders/customer/", response.statusCode(), customerId))) 38 | .bodyToMono(GetOrderResponse[].class).map(Arrays::asList) 39 | .transformDeferred(TimeLimiterOperator.of(timeLimiter)) 40 | .transformDeferred(CircuitBreakerOperator.of(circuitBreaker)) 41 | ; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/orderservice/OrderState.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.orderservice; 2 | 3 | public enum OrderState { PENDING, APPROVED, REJECTED } 4 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/apigateway/proxies/orderservice/RejectionReason.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.apigateway.proxies.orderservice; 2 | 3 | public enum RejectionReason { INSUFFICIENT_CREDIT, UNKNOWN_CUSTOMER} 4 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring.application.name: api-gateway 2 | 3 | logging.level.io.github.resilience4j: DEBUG 4 | logging.level.io.eventuate.examples: INFO 5 | logging.level.org.springframework.web: TRACE 6 | 7 | management.tracing.enabled: false 8 | management.tracing.sampling.probability: 1 9 | spring.zipkin.base.url: http://${DOCKER_HOST_IP:localhost}:9411/ 10 | 11 | apigateway.timeout.millis: 1000 12 | 13 | customer: 14 | destinations: 15 | customerServiceUrl: http://${DOCKER_HOST_IP:localhost}:8082 16 | order: 17 | destinations: 18 | orderServiceUrl: http://${DOCKER_HOST_IP:localhost}:8081 19 | 20 | resilience4j.circuitbreaker: 21 | configs: 22 | default: 23 | slidingWindowSize: 10 24 | permittedNumberOfCallsInHalfOpenState: 1 25 | waitDurationInOpenState: 10000 26 | failureRateThreshold: 60 27 | 28 | spring: 29 | cloud: 30 | gateway: 31 | routes: 32 | - id: create_customer 33 | uri: ${customer.destinations.customerServiceUrl} 34 | predicates: 35 | - Method=POST 36 | - Path=/customers 37 | filters: 38 | - CircuitBreaker=customerServiceCB 39 | - id: get_customers 40 | uri: ${customer.destinations.customerServiceUrl} 41 | predicates: 42 | - Method=GET 43 | - Path=/customers 44 | filters: 45 | - CircuitBreaker=customerServiceCB 46 | - id: get_customer 47 | uri: ${customer.destinations.customerServiceUrl} 48 | predicates: 49 | - Method=GET 50 | - Path=/customers/{customerId} 51 | filters: 52 | - CircuitBreaker=customerServiceCB 53 | - id: create_order 54 | uri: ${order.destinations.orderServiceUrl} 55 | predicates: 56 | - Method=POST 57 | - Path=/orders 58 | filters: 59 | - CircuitBreaker=orderServiceCB 60 | - id: get_order 61 | uri: ${order.destinations.orderServiceUrl} 62 | predicates: 63 | - Method=GET 64 | - Path=/orders/{orderId} 65 | filters: 66 | - CircuitBreaker=orderServiceCB 67 | - id: get_orders 68 | uri: ${order.destinations.orderServiceUrl} 69 | predicates: 70 | - Method=GET 71 | - Path=/orders 72 | filters: 73 | - CircuitBreaker=orderServiceCB 74 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/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 | -------------------------------------------------------------------------------- /api-gateway-service/api-gateway-service-main/src/main/resources/static/swagger/swagger.yml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.1 2 | paths: 3 | /customers: 4 | post: 5 | operationId: createCustomer 6 | parameters: [] 7 | requestBody: 8 | content: 9 | application/json: 10 | schema: 11 | $ref: '#/components/schemas/CreateCustomerRequest' 12 | required: true 13 | responses: 14 | default: 15 | content: 16 | application/json: 17 | schema: 18 | $ref: '#/components/schemas/CreateCustomerResponse' 19 | /customers/{customerId}: 20 | get: 21 | operationId: getCustomer 22 | parameters: 23 | - name: customerId 24 | in: path 25 | required: true 26 | schema: 27 | type: integer 28 | format: int64 29 | responses: 30 | default: 31 | content: 32 | application/json: 33 | schema: 34 | $ref: '#/components/schemas/GetCustomerResponse' 35 | /customers/{customerId}/orderhistory: 36 | get: 37 | operationId: getOrderHistory 38 | parameters: 39 | - name: customerId 40 | in: path 41 | required: true 42 | schema: 43 | type: integer 44 | format: int64 45 | responses: 46 | default: 47 | content: 48 | application/json: 49 | schema: 50 | $ref: '#/components/schemas/GetCustomerHistoryResponse' 51 | /orders: 52 | post: 53 | operationId: createOrder 54 | parameters: [] 55 | requestBody: 56 | content: 57 | application/json: 58 | schema: 59 | $ref: '#/components/schemas/CreateOrderRequest' 60 | required: true 61 | responses: 62 | default: 63 | content: 64 | application/json: 65 | schema: 66 | $ref: '#/components/schemas/CreateOrderResponse' 67 | /orders/{orderId}: 68 | get: 69 | operationId: getOrder 70 | parameters: 71 | - name: orderId 72 | in: path 73 | required: true 74 | schema: 75 | type: integer 76 | format: int64 77 | responses: 78 | default: 79 | content: 80 | application/json: 81 | schema: 82 | $ref: '#/components/schemas/GetOrderResponse' 83 | /orders/customer/{customerId}: 84 | get: 85 | operationId: getOrdersByCustomerId 86 | parameters: 87 | - name: customerId 88 | in: path 89 | required: true 90 | schema: 91 | type: integer 92 | format: int64 93 | responses: 94 | default: 95 | content: 96 | application/json: 97 | schema: 98 | type: array 99 | items: 100 | $ref: '#/components/schemas/GetOrderResponse' 101 | components: 102 | schemas: 103 | CreateCustomerResponse: 104 | type: object 105 | properties: 106 | customerId: 107 | type: integer 108 | format: int64 109 | CreateCustomerRequest: 110 | type: object 111 | properties: 112 | name: 113 | type: string 114 | creditLimit: 115 | $ref: '#/components/schemas/Money' 116 | Money: 117 | type: object 118 | properties: 119 | amount: 120 | type: number 121 | CreateOrderResponse: 122 | type: object 123 | properties: 124 | orderId: 125 | type: integer 126 | format: int64 127 | CreateOrderRequest: 128 | type: object 129 | properties: 130 | orderTotal: 131 | $ref: '#/components/schemas/Money' 132 | customerId: 133 | type: integer 134 | format: int64 135 | GetOrderResponse: 136 | type: object 137 | properties: 138 | orderId: 139 | type: integer 140 | format: int64 141 | orderState: 142 | $ref: '#/components/schemas/OrderState' 143 | GetCustomerResponse: 144 | type: object 145 | properties: 146 | customerId: 147 | type: integer 148 | format: int64 149 | name: 150 | type: string 151 | creditLimit: 152 | $ref: '#/components/schemas/Money' 153 | GetCustomerHistoryResponse: 154 | type: object 155 | properties: 156 | customerId: 157 | type: integer 158 | format: int64 159 | name: 160 | type: string 161 | creditLimit: 162 | $ref: '#/components/schemas/Money' 163 | orders: 164 | type: array 165 | items: 166 | $ref: '#/components/schemas/GetOrderResponse' 167 | OrderState: 168 | type: string 169 | enum: 170 | - PENDING 171 | - APPROVED 172 | - REJECTED -------------------------------------------------------------------------------- /build-and-test-all-mysql-sharded-outbox.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | ./gradlew -P testShardedOutbox=true endToEndTest -------------------------------------------------------------------------------- /build-and-test-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e -o pipefail 4 | 5 | ./gradlew build 6 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | maven { 5 | url "https://plugins.gradle.org/m2/" 6 | } 7 | maven { 8 | url "https://snapshots.repositories.eventuate.io/repository" 9 | } 10 | } 11 | dependencies { 12 | classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion" 13 | classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:$springCloudContractDependenciesVersion" 14 | classpath "io.eventuate.tram.testingsupport.springcloudcontract:eventuate-tram-spring-testing-support-cloud-contract-plugins-gradle:$eventuateTramSpringTestingSupportCloudContractVersion" 15 | } 16 | } 17 | 18 | 19 | apply plugin: "io.eventuate.tram.spring.testing.cloudcontract.plugins.gradle.ConfigureContractRepoDirPlugin" 20 | 21 | allprojects { 22 | group = "io.eventuate.examples.tram.sagas.customersandorders" 23 | } 24 | 25 | subprojects { 26 | 27 | apply plugin: "java-library" 28 | 29 | java { 30 | toolchain { 31 | languageVersion = JavaLanguageVersion.of(17) 32 | } 33 | } 34 | 35 | repositories { 36 | mavenCentral() 37 | eventuateMavenRepoUrl.split(',').each { repoUrl -> maven { url repoUrl } } 38 | } 39 | 40 | dependencies { 41 | implementation(platform("io.eventuate.platform:eventuate-platform-dependencies:$eventuatePlatformVersion")) 42 | 43 | implementation(platform("org.springframework.boot:spring-boot-dependencies:$springBootVersion")) 44 | 45 | implementation(platform("org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}")) 46 | 47 | // dependencyManagement { 48 | // imports { 49 | // mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 50 | // } 51 | // } 52 | 53 | // implementation(platform("org.springframework.cloud:spring-cloud-contract-dependencies:$springCloudContractDependenciesVersion")) 54 | 55 | constraints { 56 | implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:$springdocVersion" 57 | } 58 | 59 | testImplementation "org.springframework.boot:spring-boot-starter-test" 60 | 61 | } 62 | 63 | tasks.withType(Copy).all { duplicatesStrategy 'WARN' } 64 | 65 | tasks.withType(JavaCompile) { 66 | options.compilerArgs << "-parameters" 67 | } 68 | 69 | tasks.withType(Test).configureEach { 70 | useJUnitPlatform() 71 | } 72 | 73 | if (System.getenv("GRADLE_CACHE_CHANGING_VERSIONS_IN_MINUTES") != null) 74 | configurations.all { 75 | resolutionStrategy.cacheChangingModulesFor Integer.parseInt(System.getenv("GRADLE_CACHE_CHANGING_VERSIONS_IN_MINUTES")), 'minutes' 76 | } 77 | 78 | } 79 | 80 | 81 | task clean(type:Delete) { 82 | delete 'build/repos' 83 | } 84 | 85 | 86 | task endToEndTests(type: GradleBuild) { 87 | tasks = [":end-to-end-tests:endToEndTest"] 88 | dependsOn(":end-to-end-tests:clean") 89 | } 90 | 91 | 92 | task compileAll(type: GradleBuild) { 93 | tasks = ["endToEndTestClasses", "componentTestClasses", "integrationTestClasses", "testClasses", "assemble"] 94 | } 95 | 96 | task testEachService(type: GradleBuild) { 97 | tasks = ["test", "integrationTest", "contractTest", "componentTest"] 98 | } 99 | 100 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/ComponentTestsPlugin.groovy: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Plugin 2 | import org.gradle.api.Project 3 | import org.gradle.api.tasks.Copy 4 | import org.gradle.api.tasks.testing.Test 5 | 6 | class ComponentTestsPlugin implements Plugin { 7 | 8 | @Override 9 | void apply(Project project) { 10 | 11 | def copyDockerfile = project.tasks.register("copyDockerfile", Copy) { 12 | from(project.projectDir) 13 | include("Dockerfile") 14 | into(project.layout.buildDirectory.dir("generated/sources/dockerfiles")) 15 | } 16 | 17 | project.sourceSets { 18 | componentTest { 19 | java { 20 | compileClasspath += main.output + test.output 21 | runtimeClasspath += main.output + test.output 22 | srcDir project.file('src/componentTest/java') 23 | } 24 | resources.srcDir project.file('src/componentTest/resources') 25 | resources.srcDir copyDockerfile 26 | } 27 | } 28 | 29 | project.configurations { 30 | componentTestImplementation.extendsFrom testImplementation 31 | componentTestRuntime.extendsFrom testRuntime 32 | } 33 | 34 | project.dependencies { 35 | componentTestRuntimeOnly "org.junit.platform:junit-platform-launcher" 36 | } 37 | 38 | project.task("componentTest", type: Test) { 39 | testClassesDirs = project.sourceSets.componentTest.output.classesDirs 40 | classpath = project.sourceSets.componentTest.runtimeClasspath 41 | if (project.tasks.findByName("integrationTest")) 42 | shouldRunAfter("integrationTest") 43 | // Ensures that JAR is built prior to building images 44 | dependsOn("assemble") 45 | systemProperty "eventuate.servicecontainer.serviceimage.version", project.version 46 | } 47 | project.tasks.findByName("check").dependsOn(project.tasks.componentTest) 48 | 49 | 50 | project.tasks.withType(Test) { 51 | reports.html.destination = project.file("${project.reporting.baseDir}/${name}") 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/EndToEndTestsPlugin.groovy: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Plugin 2 | import org.gradle.api.Project 3 | import org.gradle.api.tasks.testing.Test 4 | 5 | class EndToEndTestsPlugin implements Plugin { 6 | 7 | @Override 8 | void apply(Project project) { 9 | 10 | project.sourceSets { 11 | endToEndTest { 12 | java { 13 | compileClasspath += project.sourceSets.main.output + project.sourceSets.test.output 14 | runtimeClasspath += project.sourceSets.main.output + project.sourceSets.test.output 15 | srcDir project.file('src/endToEndTest/java') 16 | } 17 | resources.srcDir project.file('src/endToEndTest/resources') 18 | } 19 | } 20 | 21 | project.configurations { 22 | endToEndTestImplementation.extendsFrom testImplementation 23 | endToEndTestRuntime.extendsFrom testRuntime 24 | } 25 | 26 | project.dependencies { 27 | endToEndTestRuntimeOnly "org.junit.platform:junit-platform-launcher" 28 | } 29 | 30 | project.task("endToEndTest", type: Test) { 31 | testClassesDirs = project.sourceSets.endToEndTest.output.classesDirs 32 | classpath = project.sourceSets.endToEndTest.runtimeClasspath 33 | systemProperty "eventuate.servicecontainer.serviceimage.version", project.version 34 | } 35 | 36 | project.tasks.findByName("check").dependsOn(project.tasks.endToEndTest) 37 | 38 | 39 | project.tasks.withType(Test) { 40 | reports.html.destination = project.file("${project.reporting.baseDir}/${name}") 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/IntegrationTestsPlugin.groovy: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Plugin 2 | import org.gradle.api.Project 3 | import org.gradle.api.tasks.testing.Test 4 | 5 | class IntegrationTestsPlugin implements Plugin { 6 | 7 | @Override 8 | void apply(Project project) { 9 | 10 | project.sourceSets { 11 | integrationTest { 12 | java { 13 | compileClasspath += main.output + test.output 14 | runtimeClasspath += main.output + test.output 15 | srcDir project.file('src/integrationTest/java') 16 | } 17 | resources.srcDir project.file('src/integrationTest/resources') 18 | } 19 | } 20 | 21 | project.configurations { 22 | integrationTestImplementation.extendsFrom testImplementation 23 | integrationTestRuntime.extendsFrom testRuntime 24 | } 25 | 26 | project.dependencies { 27 | integrationTestRuntimeOnly "org.junit.platform:junit-platform-launcher" 28 | } 29 | 30 | project.task("integrationTest", type: Test) { 31 | testClassesDirs = project.sourceSets.integrationTest.output.classesDirs 32 | classpath = project.sourceSets.integrationTest.runtimeClasspath 33 | shouldRunAfter("test") 34 | } 35 | project.tasks.findByName("check").dependsOn(project.tasks.integrationTest) 36 | 37 | 38 | 39 | project.tasks.withType(Test) { 40 | reports.html.destination = project.file("${project.reporting.baseDir}/${name}") 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/ServicePlugin.groovy: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Plugin 2 | import org.gradle.api.Project 3 | 4 | class ServicePlugin implements Plugin { 5 | 6 | @Override 7 | void apply(Project project) { 8 | 9 | project.apply(plugin: 'org.springframework.boot') 10 | project.apply(plugin: ComponentTestsPlugin) 11 | 12 | project.dependencies { 13 | 14 | implementation "org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j" 15 | implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui' 16 | 17 | runtimeOnly "io.eventuate.tram.springwolf:eventuate-tram-springwolf-support-starter" 18 | runtimeOnly "io.github.springwolf:springwolf-ui:${project.ext.springwolfVersion}" 19 | 20 | componentTestRuntimeOnly "io.eventuate.tram.springwolf:eventuate-tram-springwolf-support-starter" 21 | componentTestImplementation "io.eventuate.tram.springwolf:eventuate-tram-springwolf-support-testing" 22 | componentTestRuntimeOnly "io.github.springwolf:springwolf-ui:${project.ext.springwolfVersion}" 23 | 24 | } 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: IntegrationTestsPlugin 2 | apply plugin: 'org.springframework.cloud.contract' 3 | apply plugin: io.eventuate.tram.spring.testing.cloudcontract.plugins.gradle.PublishStubsPlugin 4 | 5 | 6 | dependencies { 7 | 8 | implementation "io.eventuate.examples.common:eventuate-examples-common-money-jakarta9:$eventuateCommonExamplesVersion" 9 | implementation project(":customer-service:customer-service-domain") 10 | 11 | implementation "io.eventuate.tram.core:eventuate-tram-spring-flyway" 12 | runtimeOnly "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-flyway" 13 | runtimeOnly "org.flywaydb:flyway-database-postgresql" 14 | 15 | implementation "io.eventuate.tram.core:eventuate-tram-spring-jdbc-kafka" 16 | 17 | implementation "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-participant-starter" 18 | implementation "io.eventuate.tram.core:eventuate-tram-spring-optimistic-locking" 19 | 20 | 21 | testImplementation "io.eventuate.tram.core:eventuate-tram-spring-testing-support-cloud-contract" 22 | testImplementation "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-in-memory" 23 | 24 | testImplementation "io.eventuate.messaging.kafka:eventuate-messaging-kafka-spring-producer" 25 | testImplementation "io.eventuate.tram.core:eventuate-tram-spring-logging" 26 | 27 | 28 | integrationTestImplementation "io.eventuate.common:eventuate-common-testcontainers" 29 | integrationTestImplementation "io.eventuate.messaging.kafka:eventuate-messaging-kafka-testcontainers" 30 | 31 | integrationTestImplementation project(":customer-service:customer-service-persistence") 32 | testImplementation "io.eventuate.util:eventuate-util-test" 33 | testImplementation "org.assertj:assertj-core:$assertjVersion" 34 | 35 | integrationTestImplementation "io.eventuate.tram.core:eventuate-tram-spring-testing-support-kafka-producer" 36 | integrationTestImplementation "io.eventuate.tram.core:eventuate-tram-spring-testing-support-outbox-commands" 37 | 38 | 39 | contractTestImplementation "io.eventuate.tram.testingsupport.springcloudcontract:eventuate-tram-spring-testing-support-cloud-contract:$eventuateTramSpringTestingSupportCloudContractVersion" 40 | contractTestImplementation "org.springframework.cloud:spring-cloud-starter-contract-stub-runner" 41 | } 42 | 43 | 44 | contracts { 45 | testFramework = "JUNIT5" 46 | baseClassForTests = "io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.AbstractMessagingContractTest" 47 | ignoredFiles = [ "**/*Command.groovy" ] 48 | failOnNoContracts = true 49 | } 50 | 51 | contractTest.dependsOn(publishStubsPublicationToMavenLocal) 52 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/contractTest/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/AbstractMessagingContractTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditReserved; 4 | import io.eventuate.tram.messaging.producer.MessageProducer; 5 | import io.eventuate.tram.spring.testing.cloudcontract.EnableEventuateTramContractVerifier; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | import static io.eventuate.tram.commands.consumer.CommandHandlerReplyBuilder.withSuccess; 12 | 13 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = AbstractMessagingContractTest.TestConfig.class) 14 | public abstract class AbstractMessagingContractTest { 15 | 16 | @Configuration 17 | @EnableAutoConfiguration 18 | @EnableEventuateTramContractVerifier 19 | public static class TestConfig { 20 | 21 | } 22 | 23 | @Autowired 24 | private MessageProducer messageProducer; 25 | 26 | protected void creditReserved() { 27 | messageProducer.send("reserveCreditReply", 28 | withSuccess(new CustomerCreditReserved())); 29 | } 30 | 31 | protected void reserveCredit() { 32 | // This is referenced by a disabled test 33 | throw new UnsupportedOperationException(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/contractTest/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/CommandHandlersTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerService; 5 | import io.eventuate.tram.spring.testing.cloudcontract.EnableEventuateTramContractVerifier; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.cloud.contract.stubrunner.StubFinder; 11 | import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; 12 | import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.context.annotation.Import; 15 | import org.springframework.test.annotation.DirtiesContext; 16 | import org.springframework.test.context.bean.override.mockito.MockitoBean; 17 | 18 | import static io.eventuate.util.test.async.Eventually.eventually; 19 | import static org.mockito.Mockito.verify; 20 | 21 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) 22 | @AutoConfigureStubRunner(ids = "io.eventuate.examples.tram.sagas.customersandorders:customer-service-credit-reservation-api:+", 23 | stubsMode = StubRunnerProperties.StubsMode.LOCAL) 24 | @DirtiesContext 25 | public class CommandHandlersTest { 26 | 27 | @Configuration 28 | @EnableAutoConfiguration 29 | @EnableEventuateTramContractVerifier 30 | @Import({CustomerCommandHandlerConfiguration.class}) 31 | public static class TestConfiguration { 32 | 33 | 34 | } 35 | 36 | @Autowired 37 | private StubFinder stubFinder; 38 | 39 | @MockitoBean 40 | private CustomerService customerService; 41 | 42 | @Test 43 | public void shouldReserveCredit() { 44 | stubFinder.trigger("reserveCredit"); 45 | eventually(() -> { 46 | verify(customerService).reserveCredit(101L, 102L, new Money(103)); 47 | }); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/contractTest/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://${DOCKER_HOST_IP:localhost}/eventuate 2 | spring.datasource.username=eventuate 3 | spring.datasource.password=eventuate 4 | spring.datasource.driver-class-name=org.postgresql.Driver 5 | spring.datasource.hikari.initialization-fail-timeout=30000 6 | 7 | eventuatelocal.kafka.bootstrap.servers=${DOCKER_HOST_IP:localhost}:9092 8 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/contractTest/resources/contracts/order-service/commands/reserveCreditCommand.groovy: -------------------------------------------------------------------------------- 1 | package contracts.commands 2 | 3 | org.springframework.cloud.contract.spec.Contract.make { 4 | 5 | label 'reserveCredit' 6 | input { 7 | triggeredBy('reserveCredit()') 8 | } 9 | 10 | outputMessage { 11 | sentTo('customerService') 12 | body([ 13 | customerId: 101, 14 | orderId: 102, 15 | orderTotal: 103 16 | ]) 17 | headers { 18 | header('command_type','io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.commands.ReserveCreditCommand') 19 | header('command_reply_to', 'reserveCreditReply') 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/contractTest/resources/contracts/order-service/replies/reserveCreditReply.groovy: -------------------------------------------------------------------------------- 1 | package contracts.replies 2 | 3 | org.springframework.cloud.contract.spec.Contract.make { 4 | 5 | label 'creditReserved' 6 | input { 7 | triggeredBy('creditReserved()') 8 | } 9 | 10 | outputMessage { 11 | sentTo('reserveCreditReply') 12 | body('''{}''') 13 | headers { 14 | header('reply_type', 'io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditReserved') 15 | header('reply_outcome-type', 'SUCCESS') 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/integrationTest/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/CustomerCommandHandlerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi; 2 | 3 | import io.eventuate.common.testcontainers.DatabaseContainerFactory; 4 | import io.eventuate.common.testcontainers.EventuateDatabaseContainer; 5 | import io.eventuate.examples.common.money.Money; 6 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerService; 7 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.commands.ReserveCreditCommand; 8 | import io.eventuate.messaging.kafka.testcontainers.EventuateKafkaCluster; 9 | import io.eventuate.tram.commands.producer.CommandProducer; 10 | import io.eventuate.tram.spring.flyway.EventuateTramFlywayMigrationConfiguration; 11 | import io.eventuate.tram.spring.testing.kafka.producer.EventuateKafkaTestCommandProducerConfiguration; 12 | import io.eventuate.tram.spring.testing.outbox.commands.CommandOutboxTestSupport; 13 | import io.eventuate.tram.spring.testing.outbox.commands.CommandOutboxTestSupportConfiguration; 14 | import io.eventuate.util.test.async.Eventually; 15 | import org.junit.jupiter.api.Test; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 18 | import org.springframework.boot.test.context.SpringBootTest; 19 | import org.springframework.context.annotation.Configuration; 20 | import org.springframework.context.annotation.Import; 21 | import org.springframework.test.context.DynamicPropertyRegistry; 22 | import org.springframework.test.context.DynamicPropertySource; 23 | import org.springframework.test.context.bean.override.mockito.MockitoBean; 24 | import org.testcontainers.lifecycle.Startables; 25 | 26 | import java.util.Collections; 27 | import java.util.stream.Stream; 28 | 29 | import static org.mockito.Mockito.verify; 30 | 31 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) 32 | public class CustomerCommandHandlerIntegrationTest { 33 | 34 | private static final EventuateKafkaCluster eventuateKafkaCluster = new EventuateKafkaCluster(); 35 | 36 | private static final EventuateDatabaseContainer database = DatabaseContainerFactory.makeVanillaDatabaseContainer(); 37 | 38 | @DynamicPropertySource 39 | static void registerMySqlProperties(DynamicPropertyRegistry registry) { 40 | eventuateKafkaCluster.kafka.dependsOn(eventuateKafkaCluster.zookeeper); 41 | Startables.deepStart(eventuateKafkaCluster.kafka, database).join(); 42 | 43 | Stream.of(database, eventuateKafkaCluster.zookeeper, eventuateKafkaCluster.kafka).forEach(container -> 44 | container.registerProperties(registry::add)); 45 | } 46 | 47 | // TODO - autoconfigure?? EventuateTramFlywayMigrationConfiguration 48 | 49 | @Configuration 50 | @EnableAutoConfiguration 51 | @Import({CustomerMessagingConfiguration.class, 52 | EventuateKafkaTestCommandProducerConfiguration.class, 53 | EventuateTramFlywayMigrationConfiguration.class, 54 | CommandOutboxTestSupportConfiguration.class}) 55 | static public class Config { 56 | 57 | } 58 | 59 | @MockitoBean 60 | private CustomerService customerService; 61 | 62 | 63 | @Autowired 64 | private CommandProducer commandProducer; 65 | 66 | @Autowired 67 | private CommandOutboxTestSupport commandOutboxTestSupport; 68 | 69 | @Test 70 | public void shouldHandleReserveCreditCommand() { 71 | 72 | String replyTo = "my-reply-to-channel-" + System.currentTimeMillis(); 73 | 74 | long customerId = System.currentTimeMillis(); 75 | long orderId = 102L; 76 | Money orderTotal = new Money("12.34"); 77 | 78 | sendCommand(customerId, orderId, orderTotal, replyTo); 79 | 80 | Eventually.eventually(() -> { 81 | 82 | verify(customerService).reserveCredit(customerId, orderId, orderTotal); 83 | 84 | commandOutboxTestSupport.assertCommandReplyMessageSent(replyTo); 85 | }); 86 | } 87 | 88 | private void sendCommand(long customerId, long orderId, Money orderTotal, String replyTo) { 89 | commandProducer.send("customerService", new ReserveCreditCommand(customerId, orderId, orderTotal), replyTo, Collections.emptyMap()); 90 | } 91 | 92 | 93 | } 94 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/integrationTest/resources/application-postgres.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://${DOCKER_HOST_IP:localhost}/eventuate 2 | spring.datasource.username=eventuate 3 | spring.datasource.password=eventuate 4 | spring.datasource.driver-class-name=org.postgresql.Driver 5 | eventuate.database.schema=public -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/integrationTest/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=order-service 2 | spring.jpa.generate-ddl=true 3 | logging.level.org.springframework.orm.jpa=INFO 4 | logging.level.org.hibernate.SQL=DEBUG 5 | logging.level.io.eventuate=DEBUG 6 | logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG 7 | 8 | eventuatelocal.kafka.bootstrap.servers=${DOCKER_HOST_IP:localhost}:9092 9 | eventuatelocal.zookeeper.connection.string=${DOCKER_HOST_IP:localhost}:2181 10 | 11 | spring.datasource.url=jdbc:mysql://${DOCKER_HOST_IP:localhost}/customer_service 12 | spring.datasource.username=mysqluser 13 | spring.datasource.password=mysqlpw 14 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 15 | 16 | management.tracing.enabled=true 17 | management.tracing.sampling.probability=1 18 | spring.zipkin.base.url=http://${DOCKER_HOST_IP:localhost}:9411/ 19 | 20 | spring.flyway.locations=classpath:flyway/{vendor} 21 | spring.flyway.baseline-on-migrate=true 22 | spring.flyway.baseline-version=0 23 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/CustomerCommandHandler.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerCreditLimitExceededException; 4 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerNotFoundException; 5 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerService; 6 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.commands.ReserveCreditCommand; 7 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditLimitExceeded; 8 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditReserved; 9 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerNotFound; 10 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.ReserveCreditResult; 11 | import io.eventuate.tram.commands.consumer.CommandMessage; 12 | import io.eventuate.tram.commands.consumer.annotations.EventuateCommandHandler; 13 | 14 | public class CustomerCommandHandler { 15 | 16 | private final CustomerService customerService; 17 | 18 | public CustomerCommandHandler(CustomerService customerService) { 19 | this.customerService = customerService; 20 | } 21 | 22 | @EventuateCommandHandler(subscriberId="customerCommandDispatcher", channel="customerService") 23 | public ReserveCreditResult reserveCredit(CommandMessage cm) { 24 | ReserveCreditCommand cmd = cm.getCommand(); 25 | try { 26 | customerService.reserveCredit(cmd.customerId(), cmd.orderId(), cmd.orderTotal()); 27 | return new CustomerCreditReserved(); 28 | } catch (CustomerNotFoundException e) { 29 | return new CustomerNotFound(); 30 | } catch (CustomerCreditLimitExceededException e) { 31 | return new CustomerCreditLimitExceeded(); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/CustomerCommandHandlerConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerService; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | public class CustomerCommandHandlerConfiguration { 9 | 10 | @Bean 11 | public CustomerCommandHandler customerCommandHandler(CustomerService customerService) { 12 | return new CustomerCommandHandler(customerService); 13 | } 14 | 15 | // TODO Exception handler for CustomerCreditLimitExceededException 16 | } 17 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/CustomerMessagingConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi; 2 | 3 | import io.eventuate.tram.spring.flyway.EventuateTramFlywayMigrationConfiguration; 4 | import io.eventuate.tram.spring.optimisticlocking.OptimisticLockingDecoratorConfiguration; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Import; 7 | 8 | @Configuration 9 | @Import({OptimisticLockingDecoratorConfiguration.class, EventuateTramFlywayMigrationConfiguration.class, 10 | CustomerCommandHandlerConfiguration.class}) 11 | public class CustomerMessagingConfiguration { 12 | 13 | 14 | 15 | } 16 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/commands/ReserveCreditCommand.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.commands; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | import io.eventuate.tram.commands.common.Command; 5 | 6 | public record ReserveCreditCommand(Long customerId, Long orderId, Money orderTotal) implements Command { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/replies/CustomerCreditLimitExceeded.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies; 2 | 3 | import io.eventuate.tram.commands.consumer.annotations.FailureReply; 4 | 5 | @FailureReply 6 | public class CustomerCreditLimitExceeded implements ReserveCreditResult {} 7 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/replies/CustomerCreditReserved.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies; 2 | 3 | import io.eventuate.tram.commands.consumer.annotations.SuccessReply; 4 | 5 | @SuccessReply 6 | public class CustomerCreditReserved implements ReserveCreditResult { } 7 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/replies/CustomerNotFound.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies; 2 | 3 | import io.eventuate.tram.commands.consumer.annotations.FailureReply; 4 | 5 | @FailureReply 6 | public class CustomerNotFound implements ReserveCreditResult {} 7 | -------------------------------------------------------------------------------- /customer-service/customer-service-credit-reservation-api/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/replies/ReserveCreditResult.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies; 2 | 3 | public interface ReserveCreditResult { 4 | } 5 | -------------------------------------------------------------------------------- /customer-service/customer-service-domain/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | api "io.eventuate.examples.common:eventuate-examples-common-money-jakarta9:$eventuateCommonExamplesVersion" 3 | implementation "org.springframework.boot:spring-boot-starter-data-jpa" 4 | } 5 | 6 | -------------------------------------------------------------------------------- /customer-service/customer-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/domain/Customer.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.domain; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | 5 | import jakarta.persistence.*; 6 | import java.util.Collections; 7 | import java.util.Map; 8 | 9 | @Entity 10 | @Table(name="Customer") 11 | @Access(AccessType.FIELD) 12 | public class Customer { 13 | 14 | @Id 15 | @GeneratedValue(strategy = GenerationType.IDENTITY) 16 | private Long id; 17 | private String name; 18 | 19 | @Embedded 20 | private Money creditLimit; 21 | 22 | @ElementCollection 23 | private Map creditReservations; 24 | 25 | @Version 26 | private Long version; 27 | 28 | public Money availableCredit() { 29 | return creditLimit.subtract(creditReservations.values().stream().reduce(Money.ZERO, Money::add)); 30 | } 31 | 32 | public Customer() { 33 | } 34 | 35 | public Customer(String name, Money creditLimit) { 36 | this.name = name; 37 | this.creditLimit = creditLimit; 38 | this.creditReservations = Collections.emptyMap(); 39 | } 40 | 41 | public Long getId() { 42 | return id; 43 | } 44 | 45 | public String getName() { 46 | return name; 47 | } 48 | 49 | public Money getCreditLimit() { 50 | return creditLimit; 51 | } 52 | 53 | public void reserveCredit(Long orderId, Money orderTotal) { 54 | if (availableCredit().isGreaterThanOrEqual(orderTotal)) { 55 | creditReservations.put(orderId, orderTotal); 56 | } else 57 | throw new CustomerCreditLimitExceededException(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /customer-service/customer-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/domain/CustomerCreditLimitExceededException.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.domain; 2 | 3 | public class CustomerCreditLimitExceededException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /customer-service/customer-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/domain/CustomerDomainConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.domain; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | public class CustomerDomainConfiguration { 8 | 9 | @Bean 10 | public CustomerService customerService(CustomerRepository customerRepository) { 11 | return new CustomerService(customerRepository); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /customer-service/customer-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/domain/CustomerNotFoundException.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.domain; 2 | 3 | public class CustomerNotFoundException extends RuntimeException { 4 | } 5 | -------------------------------------------------------------------------------- /customer-service/customer-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/domain/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.domain; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | public interface CustomerRepository extends CrudRepository { 6 | } 7 | -------------------------------------------------------------------------------- /customer-service/customer-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/domain/CustomerService.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.domain; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | 5 | import jakarta.transaction.Transactional; 6 | 7 | public class CustomerService { 8 | 9 | private CustomerRepository customerRepository; 10 | 11 | public CustomerService(CustomerRepository customerRepository) { 12 | this.customerRepository = customerRepository; 13 | } 14 | 15 | @Transactional 16 | public Customer createCustomer(String name, Money creditLimit) { 17 | Customer customer = new Customer(name, creditLimit); 18 | return customerRepository.save(customer); 19 | } 20 | 21 | public void reserveCredit(long customerId, long orderId, Money orderTotal) throws CustomerCreditLimitExceededException { 22 | Customer customer = customerRepository.findById(customerId).orElseThrow(CustomerNotFoundException::new); 23 | customer.reserveCredit(orderId, orderTotal); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /customer-service/customer-service-main/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG baseImageVersion 2 | FROM eventuateio/eventuate-examples-docker-images-spring-example-base-image:$baseImageVersion 3 | ARG serviceImageVersion 4 | COPY build/libs/customer-service-main-$serviceImageVersion.jar service.jar 5 | -------------------------------------------------------------------------------- /customer-service/customer-service-main/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: ServicePlugin 2 | 3 | dependencies { 4 | implementation project(":customer-service:customer-service-domain") 5 | implementation project(":customer-service:customer-service-restapi") 6 | implementation project(":customer-service:customer-service-persistence") 7 | implementation project(":customer-service:customer-service-credit-reservation-api") 8 | implementation "io.eventuate.tram.core:eventuate-tram-spring-logging" 9 | 10 | implementation "org.springframework.boot:spring-boot-starter-actuator" 11 | implementation "org.springframework.boot:spring-boot-starter-web" 12 | 13 | componentTestImplementation "io.eventuate.platform.testcontainer.support:eventuate-platform-testcontainer-support-service:$eventuatePlatformTestContainerSupportVersion" 14 | 15 | componentTestImplementation "io.eventuate.common:eventuate-common-testcontainers" 16 | componentTestImplementation "io.eventuate.messaging.kafka:eventuate-messaging-kafka-testcontainers" 17 | componentTestImplementation "io.eventuate.cdc:eventuate-cdc-testcontainers" 18 | componentTestImplementation "io.rest-assured:rest-assured" 19 | componentTestImplementation "io.eventuate.tram.core:eventuate-tram-spring-in-memory" 20 | componentTestImplementation "io.eventuate.tram.springwolf:eventuate-tram-springwolf-support-testing" 21 | 22 | } 23 | 24 | 25 | check.shouldRunAfter(":customer-service:customer-service-restapi:check") 26 | check.shouldRunAfter(":customer-service:customer-service-persistence:check") 27 | check.shouldRunAfter(":customer-service:customer-service-credit-reservation-api:check") 28 | check.shouldRunAfter(":customer-service:customer-service-credit-reservation-api:check") 29 | -------------------------------------------------------------------------------- /customer-service/customer-service-main/src/componentTest/java/io/eventuate/examples/tram/sagas/customersandorders/customers/CustomerServiceComponentTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers; 2 | 3 | 4 | import io.eventuate.common.testcontainers.ContainerTestUtil; 5 | import io.eventuate.common.testcontainers.DatabaseContainerFactory; 6 | import io.eventuate.common.testcontainers.EventuateDatabaseContainer; 7 | import io.eventuate.messaging.kafka.testcontainers.EventuateKafkaNativeCluster; 8 | import io.eventuate.messaging.kafka.testcontainers.EventuateKafkaNativeContainer; 9 | import io.eventuate.testcontainers.service.ServiceContainer; 10 | import org.junit.jupiter.api.Assertions; 11 | import org.junit.jupiter.api.BeforeAll; 12 | import org.junit.jupiter.api.Test; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | import org.testcontainers.containers.output.Slf4jLogConsumer; 16 | import org.testcontainers.lifecycle.Startables; 17 | 18 | import java.io.IOException; 19 | import java.net.HttpURLConnection; 20 | import java.net.URL; 21 | 22 | public class CustomerServiceComponentTest { 23 | 24 | protected static Logger logger = LoggerFactory.getLogger(CustomerServiceComponentTest.class); 25 | 26 | public static EventuateKafkaNativeCluster eventuateKafkaCluster = new EventuateKafkaNativeCluster("customer-service-tests"); 27 | 28 | public static EventuateKafkaNativeContainer kafka = eventuateKafkaCluster.kafka 29 | .withNetworkAliases("kafka") 30 | .withReuse(ContainerTestUtil.shouldReuse()); 31 | 32 | public static EventuateDatabaseContainer database 33 | = DatabaseContainerFactory.makeVanillaDatabaseContainer() 34 | .withNetwork(eventuateKafkaCluster.network) 35 | .withNetworkAliases("customer-service-mysql") 36 | .withReuse(ContainerTestUtil.shouldReuse()); 37 | 38 | 39 | public static ServiceContainer service = 40 | ServiceContainer.makeFromDockerfileOnClasspath() 41 | .withNetwork(eventuateKafkaCluster.network) 42 | .withDatabase(database) 43 | .withKafka(kafka) 44 | .withLogConsumer(new Slf4jLogConsumer(logger).withPrefix("SVC customer-service:")) 45 | .withReuse(false) // should rebuild 46 | ; 47 | 48 | @BeforeAll 49 | public static void startContainers() { 50 | Startables.deepStart(service).join(); 51 | } 52 | 53 | @Test 54 | public void shouldStart() { 55 | // HTTP 56 | // Messaging 57 | } 58 | 59 | @Test 60 | void shouldExposeSwaggerUI() throws IOException { 61 | String url = "http://%s:%s/swagger-ui/index.html".formatted("localhost", service.getFirstMappedPort()); 62 | HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); 63 | if (connection.getResponseCode() != 200) 64 | Assertions.fail("%s: Expected 200 for %s, got %s".formatted("Customer Service", url, connection.getResponseCode())); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /customer-service/customer-service-main/src/componentTest/java/io/eventuate/examples/tram/sagas/customersandorders/customers/CustomerServiceInProcessComponentTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers; 2 | 3 | 4 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerDomainConfiguration; 5 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.CustomerCommandHandler; 6 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.CustomerCommandHandlerConfiguration; 7 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.commands.ReserveCreditCommand; 8 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditLimitExceeded; 9 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditReserved; 10 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerNotFound; 11 | import io.eventuate.examples.tram.sagas.customersandorders.customers.persistence.CustomerPersistenceConfiguration; 12 | import io.eventuate.examples.tram.sagas.customersandorders.customers.restapi.CustomerRestApiConfiguration; 13 | import io.eventuate.tram.spring.inmemory.TramInMemoryConfiguration; 14 | import io.eventuate.tram.spring.springwolf.testing.AsyncApiDocument; 15 | import io.restassured.RestAssured; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 21 | import org.springframework.boot.test.context.SpringBootTest; 22 | import org.springframework.boot.test.web.server.LocalServerPort; 23 | import org.springframework.context.annotation.Configuration; 24 | import org.springframework.context.annotation.Import; 25 | 26 | import java.util.Set; 27 | 28 | 29 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 30 | public class CustomerServiceInProcessComponentTest { 31 | 32 | protected static Logger logger = LoggerFactory.getLogger(CustomerServiceInProcessComponentTest.class); 33 | 34 | @Configuration 35 | @EnableAutoConfiguration 36 | @Import({CustomerRestApiConfiguration.class, CustomerPersistenceConfiguration.class, 37 | CustomerDomainConfiguration.class, 38 | CustomerCommandHandlerConfiguration.class, 39 | TramInMemoryConfiguration.class 40 | }) 41 | static public class Config { 42 | 43 | } 44 | 45 | @LocalServerPort 46 | private int port; 47 | 48 | @BeforeEach 49 | public void setup() { 50 | RestAssured.port = port; 51 | } 52 | 53 | @Test 54 | public void shouldStart() { 55 | } 56 | 57 | @Test 58 | void shouldExposeSwaggerUI() { 59 | RestAssured.given() 60 | .get("/swagger-ui/index.html") 61 | .then() 62 | .statusCode(200); 63 | } 64 | 65 | @Test 66 | public void shouldExposeSpringWolf() { 67 | AsyncApiDocument doc = AsyncApiDocument.getSpringWolfDoc(); 68 | 69 | doc.assertReceivesMessageAndReplies(CustomerCommandHandler.class.getName() + ".reserveCredit", 70 | "customerService", 71 | ReserveCreditCommand.class.getName(), 72 | Set.of(CustomerNotFound.class.getName(), CustomerCreditLimitExceeded.class.getName(), CustomerCreditReserved.class.getName())); 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /customer-service/customer-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/CustomerServiceMain.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.boot.SpringApplication; 10 | import org.springframework.boot.autoconfigure.SpringBootApplication; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.web.filter.OncePerRequestFilter; 13 | 14 | import java.io.IOException; 15 | 16 | @SpringBootApplication 17 | public class CustomerServiceMain { 18 | 19 | private Logger _logger = LoggerFactory.getLogger(getClass()); 20 | 21 | 22 | @Bean 23 | public OncePerRequestFilter logFilter() { 24 | return new OncePerRequestFilter() { 25 | @Override 26 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 27 | _logger.info("Path: {}", request.getRequestURI()); 28 | filterChain.doFilter(request, response); 29 | _logger.info("Path: {} {}", request.getRequestURI(), response.getStatus()); 30 | } 31 | }; 32 | } 33 | 34 | public static void main(String[] args) { 35 | SpringApplication.run(CustomerServiceMain.class, args); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /customer-service/customer-service-main/src/main/resources/REMOVE-ME: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eventuate-tram/eventuate-tram-sagas-examples-customers-and-orders/ffe523909be3f46049d2feadf28d7c257da68e96/customer-service/customer-service-main/src/main/resources/REMOVE-ME -------------------------------------------------------------------------------- /customer-service/customer-service-main/src/main/resources/application-postgres.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://${DOCKER_HOST_IP:localhost}/eventuate 2 | spring.datasource.username=eventuate 3 | spring.datasource.password=eventuate 4 | spring.datasource.driver-class-name=org.postgresql.Driver 5 | spring.datasource.hikari.initialization-fail-timeout=30000 6 | 7 | # Copy/paste 8 | 9 | spring.flyway.locations=classpath:flyway/{vendor} 10 | spring.flyway.baseline-on-migrate=true 11 | spring.flyway.baseline-version=0 12 | -------------------------------------------------------------------------------- /customer-service/customer-service-main/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=customer-service 2 | spring.jpa.generate-ddl=true 3 | logging.level.org.springframework.orm.jpa=INFO 4 | logging.level.org.hibernate.SQL=DEBUG 5 | logging.level.io.eventuate=DEBUG 6 | logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG 7 | logging.level.org.springframework.boot.autoconfigure=DEBUG 8 | 9 | eventuatelocal.kafka.bootstrap.servers=${DOCKER_HOST_IP:localhost}:9092 10 | eventuatelocal.zookeeper.connection.string=${DOCKER_HOST_IP:localhost}:2181 11 | 12 | spring.datasource.url=jdbc:mysql://${DOCKER_HOST_IP:localhost}/SHOULD_BE_CUSTOMER_SERVICE 13 | spring.datasource.username=mysqluser 14 | spring.datasource.password=mysqlpw 15 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 16 | spring.datasource.hikari.initialization-fail-timeout=30000 17 | 18 | management.tracing.enabled=true 19 | management.tracing.sampling.probability=1 20 | spring.zipkin.base.url=http://${DOCKER_HOST_IP:localhost}:9411/ 21 | 22 | # Copy/paste 23 | 24 | spring.flyway.locations=classpath:flyway/{vendor} 25 | spring.flyway.baseline-on-migrate=true 26 | spring.flyway.baseline-version=0 27 | 28 | springwolf.docket.base-package=io.eventuate.examples.tram.sagas.ordersandcustomers 29 | 30 | springwolf.docket.info.title=${spring.application.name} 31 | springwolf.docket.info.version=1.0.0 32 | springwolf.docket.scanner.async-listener.enabled=false 33 | 34 | springwolf.docket.servers.eventuate-producer.protocol=eventuate-outbox 35 | springwolf.docket.servers.eventuate-producer.host=${spring.datasource.url} 36 | springwolf.docket.servers.eventuate-consumer.protocol=kafka 37 | springwolf.docket.servers.eventuate-consumer.host=${eventuatelocal.kafka.bootstrap.servers} 38 | -------------------------------------------------------------------------------- /customer-service/customer-service-main/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /customer-service/customer-service-persistence/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: IntegrationTestsPlugin 2 | 3 | dependencies { 4 | implementation project(":customer-service:customer-service-domain") 5 | 6 | //implementation "io.eventuate.common:eventuate-common-jdbc" 7 | 8 | implementation "org.springframework.boot:spring-boot-starter-data-jpa" 9 | 10 | implementation 'com.mysql:mysql-connector-j:8.0.33' 11 | implementation 'org.postgresql:postgresql:9.4-1200-jdbc41' 12 | 13 | 14 | integrationTestImplementation "io.eventuate.common:eventuate-common-testcontainers" 15 | 16 | } 17 | 18 | task("postgresIntegrationTest", type: Test) { 19 | testClassesDirs = sourceSets.integrationTest.output.classesDirs 20 | classpath = sourceSets.integrationTest.runtimeClasspath 21 | shouldRunAfter test 22 | } 23 | 24 | check.dependsOn postgresIntegrationTest 25 | 26 | postgresIntegrationTest { 27 | systemProperty "spring.profiles.active", "postgres" 28 | } 29 | -------------------------------------------------------------------------------- /customer-service/customer-service-persistence/src/integrationTest/java/io/eventuate/examples/tram/sagas/customersandorders/customers/persistence/CustomerServiceRepositoriesTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.persistence; 2 | 3 | import io.eventuate.common.testcontainers.ContainerTestUtil; 4 | import io.eventuate.common.testcontainers.DatabaseContainerFactory; 5 | import io.eventuate.common.testcontainers.EventuateDatabaseContainer; 6 | import io.eventuate.common.testcontainers.PropertyProvidingContainer; 7 | import io.eventuate.examples.common.money.Money; 8 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.Customer; 9 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerRepository; 10 | import org.junit.jupiter.api.Test; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 13 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.context.annotation.Import; 16 | import org.springframework.test.context.ContextConfiguration; 17 | import org.springframework.test.context.DynamicPropertyRegistry; 18 | import org.springframework.test.context.DynamicPropertySource; 19 | import org.springframework.transaction.annotation.Propagation; 20 | import org.springframework.transaction.annotation.Transactional; 21 | import org.springframework.transaction.support.TransactionTemplate; 22 | 23 | import static org.junit.jupiter.api.Assertions.assertEquals; 24 | 25 | @DataJpaTest 26 | @ContextConfiguration(classes= CustomerServiceRepositoriesTest.Config.class) 27 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 28 | @Transactional(propagation = Propagation.NEVER) 29 | public class CustomerServiceRepositoriesTest { 30 | 31 | 32 | public static EventuateDatabaseContainer database = 33 | DatabaseContainerFactory.makeVanillaDatabaseContainer() 34 | .withReuse(ContainerTestUtil.shouldReuse()); 35 | 36 | @DynamicPropertySource 37 | static void registerMySqlProperties(DynamicPropertyRegistry registry) { 38 | PropertyProvidingContainer.startAndProvideProperties(registry, database); 39 | } 40 | 41 | public static final String customerName = "Chris"; 42 | 43 | @Configuration 44 | @Import(CustomerPersistenceConfiguration.class) 45 | static public class Config { 46 | } 47 | 48 | @Autowired 49 | private CustomerRepository customerRepository; 50 | 51 | @Autowired 52 | private TransactionTemplate transactionTemplate; 53 | 54 | @Test 55 | public void shouldSaveAndLoadCustomer() { 56 | Money creditLimit = new Money("12.34"); 57 | Money amount = new Money("10"); 58 | Money expectedAvailableCredit = creditLimit.subtract(amount); 59 | 60 | Customer c = new Customer(customerName, creditLimit); 61 | 62 | transactionTemplate.executeWithoutResult( ts -> customerRepository.save(c) ); 63 | 64 | transactionTemplate.executeWithoutResult(ts -> { 65 | Customer c2 = customerRepository.findById(c.getId()).get(); 66 | assertEquals(customerName, c2.getName()); 67 | assertEquals(creditLimit, c2.getCreditLimit()); 68 | assertEquals(creditLimit, c2.availableCredit()); 69 | 70 | c2.reserveCredit(1234L, amount); 71 | }); 72 | 73 | transactionTemplate.executeWithoutResult(ts -> { 74 | Customer c2 = customerRepository.findById(c.getId()).get(); 75 | assertEquals(expectedAvailableCredit, c2.availableCredit()); 76 | 77 | }); 78 | 79 | 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /customer-service/customer-service-persistence/src/integrationTest/resources/application-postgres.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://${DOCKER_HOST_IP:localhost}/eventuate 2 | spring.datasource.username=eventuate 3 | spring.datasource.password=eventuate 4 | spring.datasource.driver-class-name=org.postgresql.Driver -------------------------------------------------------------------------------- /customer-service/customer-service-persistence/src/integrationTest/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=order-service 2 | spring.jpa.generate-ddl=true 3 | logging.level.org.springframework.orm.jpa=INFO 4 | logging.level.org.hibernate.SQL=DEBUG 5 | logging.level.io.eventuate=DEBUG 6 | logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG 7 | 8 | eventuatelocal.kafka.bootstrap.servers=${DOCKER_HOST_IP:localhost}:9092 9 | eventuatelocal.zookeeper.connection.string=${DOCKER_HOST_IP:localhost}:2181 10 | 11 | spring.datasource.url=jdbc:mysql://${DOCKER_HOST_IP:localhost}/customer_service 12 | spring.datasource.username=mysqluser 13 | spring.datasource.password=mysqlpw 14 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 15 | 16 | management.tracing.enabled=true 17 | management.tracing.sampling.probability=1 18 | spring.zipkin.base.url=http://${DOCKER_HOST_IP:localhost}:9411/ 19 | 20 | spring.flyway.locations=classpath:flyway/{vendor} 21 | spring.flyway.baseline-on-migrate=true 22 | spring.flyway.baseline-version=0 23 | -------------------------------------------------------------------------------- /customer-service/customer-service-persistence/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/persistence/CustomerPersistenceConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.persistence; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.Customer; 4 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerRepository; 5 | import org.springframework.boot.autoconfigure.domain.EntityScan; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 8 | 9 | @Configuration 10 | @EnableJpaRepositories(basePackageClasses = {CustomerRepository.class}) 11 | @EntityScan(basePackageClasses = {Customer.class}) 12 | public class CustomerPersistenceConfiguration { 13 | 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /customer-service/customer-service-restapi/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'spring-cloud-contract' 2 | apply plugin: io.eventuate.tram.spring.testing.cloudcontract.plugins.gradle.PublishStubsPlugin 3 | 4 | contracts { 5 | packageWithBaseClasses = 'io.eventuate.examples.tram.sagas.customersandorders.customers.restapi' 6 | } 7 | 8 | dependencies { 9 | 10 | implementation project(":customer-service:customer-service-domain") 11 | 12 | implementation "io.eventuate.examples.common:eventuate-examples-common-money-jakarta9:$eventuateCommonExamplesVersion" 13 | 14 | implementation "org.springframework.boot:spring-boot-starter-web" 15 | implementation "org.springframework.boot:spring-boot-starter-data-jpa" 16 | 17 | testImplementation "org.springframework.cloud:spring-cloud-starter-contract-verifier" 18 | testImplementation "io.rest-assured:spring-mock-mvc" 19 | 20 | } 21 | 22 | check.dependsOn(contractTest) -------------------------------------------------------------------------------- /customer-service/customer-service-restapi/src/contractTest/java/io/eventuate/examples/tram/sagas/customersandorders/customers/restapi/ApigatewayBase.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.restapi; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.Customer; 5 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerRepository; 6 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerService; 7 | import io.restassured.module.mockmvc.RestAssuredMockMvc; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.springframework.test.util.ReflectionTestUtils; 10 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 11 | 12 | import java.util.Optional; 13 | 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.when; 16 | 17 | public abstract class ApigatewayBase { 18 | 19 | @BeforeEach 20 | public void setup() { 21 | CustomerService customerService = mock(CustomerService.class); 22 | CustomerRepository customerRepository = mock(CustomerRepository.class); 23 | CustomerController orderController = new CustomerController(customerService, customerRepository); 24 | 25 | Customer customer = new Customer("Chris", new Money("123.45")); 26 | ReflectionTestUtils.setField(customer, "id", 101L); 27 | 28 | when(customerService.createCustomer(customer.getName(), customer.getCreditLimit())).thenReturn(customer); 29 | when(customerRepository.findById(customer.getId())).thenReturn(Optional.of(customer)); 30 | 31 | RestAssuredMockMvc.standaloneSetup(MockMvcBuilders.standaloneSetup(orderController)); 32 | 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /customer-service/customer-service-restapi/src/contractTest/resources/contracts/apigateway/createCustomer.groovy: -------------------------------------------------------------------------------- 1 | package contracts.apigateway 2 | 3 | import org.springframework.cloud.contract.spec.Contract; 4 | 5 | Contract.make { 6 | request { 7 | method 'POST' 8 | url '/customers' 9 | body('''{"name" : "Chris", "creditLimit" : { "amount" : "123.45" }}''') 10 | headers{ 11 | header('Content-Type': 'application/json') 12 | } 13 | } 14 | 15 | response { 16 | status 200 17 | headers { 18 | header('Content-Type': 'application/json') 19 | } 20 | body('''{"customerId" : "101"}''') 21 | } 22 | } -------------------------------------------------------------------------------- /customer-service/customer-service-restapi/src/contractTest/resources/contracts/apigateway/getCustomer.groovy: -------------------------------------------------------------------------------- 1 | package contracts.http; 2 | 3 | org.springframework.cloud.contract.spec.Contract.make { 4 | request { 5 | method 'GET' 6 | url '/customers/101' 7 | } 8 | response { 9 | status 200 10 | headers { 11 | header('Content-Type': 'application/json') 12 | } 13 | body('''{"customerId" : "101", "name" : "Chris", "creditLimit" : { "amount" : "123.45" }}''') 14 | } 15 | } -------------------------------------------------------------------------------- /customer-service/customer-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/restapi/CreateCustomerRequest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.restapi; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | 5 | public record CreateCustomerRequest(String name, Money creditLimit) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /customer-service/customer-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/restapi/CreateCustomerResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.restapi; 2 | 3 | 4 | public record CreateCustomerResponse(Long customerId) { 5 | } 6 | -------------------------------------------------------------------------------- /customer-service/customer-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/restapi/CustomerController.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.restapi; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.Customer; 4 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerRepository; 5 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerService; 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.util.stream.Collectors; 11 | import java.util.stream.StreamSupport; 12 | 13 | @RestController 14 | public class CustomerController { 15 | 16 | private final CustomerService customerService; 17 | private final CustomerRepository customerRepository; 18 | 19 | public CustomerController(CustomerService customerService, CustomerRepository customerRepository) { 20 | this.customerService = customerService; 21 | this.customerRepository = customerRepository; 22 | } 23 | 24 | @PostMapping("/customers") 25 | public CreateCustomerResponse createCustomer(@RequestBody CreateCustomerRequest createCustomerRequest) { 26 | Customer customer = customerService.createCustomer(createCustomerRequest.name(), createCustomerRequest.creditLimit()); 27 | return new CreateCustomerResponse(customer.getId()); 28 | } 29 | 30 | @GetMapping("/customers") 31 | public ResponseEntity getAll() { 32 | return ResponseEntity.ok(new GetCustomersResponse(StreamSupport.stream(customerRepository.findAll().spliterator(), false) 33 | .map(c -> new GetCustomerResponse(c.getId(), c.getName(), c.getCreditLimit())).collect(Collectors.toList()))); 34 | } 35 | 36 | @GetMapping("/customers/{customerId}") 37 | public ResponseEntity getCustomer(@PathVariable Long customerId) { 38 | return customerRepository 39 | .findById(customerId) 40 | .map(c -> new ResponseEntity<>(new GetCustomerResponse(c.getId(), c.getName(), c.getCreditLimit()), HttpStatus.OK)) 41 | .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /customer-service/customer-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/restapi/CustomerRestApiConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.restapi; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @ComponentScan 8 | public class CustomerRestApiConfiguration { 9 | 10 | } -------------------------------------------------------------------------------- /customer-service/customer-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/restapi/GetCustomerResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.restapi; 2 | 3 | 4 | import io.eventuate.examples.common.money.Money; 5 | 6 | public record GetCustomerResponse(Long customerId, String name, Money creditLimit) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /customer-service/customer-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/restapi/GetCustomersResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.restapi; 2 | 3 | import java.util.List; 4 | 5 | public record GetCustomersResponse(List customers) { 6 | } 7 | -------------------------------------------------------------------------------- /customer-service/customer-service-restapi/src/test/java/io/eventuate/examples/tram/sagas/customersandorders/customers/restapi/CustomerControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.restapi; 2 | 3 | 4 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerRepository; 5 | import io.eventuate.examples.tram.sagas.customersandorders.customers.domain.CustomerService; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.mockito.InjectMocks; 9 | import org.mockito.Mock; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | import org.springframework.http.HttpStatus; 12 | 13 | import java.util.Collections; 14 | 15 | import static io.restassured.http.ContentType.JSON; 16 | import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; 17 | import static org.hamcrest.Matchers.empty; 18 | import static org.mockito.Mockito.when; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | public class CustomerControllerTest { 22 | 23 | @Mock 24 | private CustomerService customerService; 25 | @Mock 26 | private CustomerRepository customerRepository; 27 | 28 | @InjectMocks 29 | private CustomerController customerController; 30 | 31 | @Test 32 | public void shouldGetCustomers() { 33 | when(customerRepository.findAll()).thenReturn(Collections.emptyList()); 34 | 35 | given() 36 | .standaloneSetup(customerController) 37 | .when() 38 | .get("/customers") 39 | .then() 40 | .log().ifValidationFails() 41 | .statusCode(HttpStatus.OK.value()) 42 | .contentType(JSON) 43 | .and().body("customers", empty()); 44 | } 45 | } -------------------------------------------------------------------------------- /end-to-end-tests/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: EndToEndTestsPlugin 2 | 3 | def copyDockerfiles = tasks.register("copyDockerfiles", Copy) { 4 | from(project.rootDir) 5 | include("customer-service/customer-service-main/Dockerfile", "order-service/order-service-main/Dockerfile", "api-gateway-service/api-gateway-service-main/Dockerfile") 6 | into(project.layout.buildDirectory.dir("generated/sources/dockerfiles")) 7 | } 8 | 9 | // Create a dependency on the Dockerfiles without using them 10 | 11 | sourceSets { 12 | endToEndTest { 13 | resources.srcDir copyDockerfiles 14 | } 15 | } 16 | 17 | dependencies { 18 | testImplementation "io.eventuate.examples.common:eventuate-examples-common-money-jakarta9:$eventuateCommonExamplesVersion" 19 | 20 | testImplementation "org.springframework.boot:spring-boot-starter-web" 21 | testImplementation "org.springframework.boot:spring-boot-starter-test" 22 | 23 | testImplementation "io.eventuate.util:eventuate-util-test" 24 | 25 | endToEndTestImplementation "io.eventuate.platform.testcontainer.support:eventuate-platform-testcontainer-support-service:$eventuatePlatformTestContainerSupportVersion" 26 | endToEndTestImplementation "io.eventuate.cdc:eventuate-cdc-testcontainers" 27 | endToEndTestImplementation "io.eventuate.common:eventuate-common-testcontainers" 28 | endToEndTestImplementation "io.eventuate.messaging.kafka:eventuate-messaging-kafka-testcontainers" 29 | endToEndTestImplementation "org.springframework.boot:spring-boot-starter-thymeleaf" 30 | } 31 | 32 | endToEndTest.dependsOn(":customer-service:customer-service-main:assemble", ":order-service:order-service-main:assemble", ":api-gateway-service:api-gateway-service-main:assemble") 33 | check.shouldRunAfter(":customer-service:customer-service-main:check") 34 | check.shouldRunAfter(":order-service:order-service-main:check") 35 | check.shouldRunAfter(":api-gateway-service:api-gateway-service-main:check") 36 | 37 | endToEndTest { 38 | if (project.hasProperty('endToEndTestMode')) 39 | systemProperty "endToEndTestMode", endToEndTestMode 40 | 41 | } 42 | 43 | for (def dbType in ["MySql", "Postgres"]) { 44 | def springProfilesActive = dbType.toLowerCase() 45 | tasks.register("runApplication${dbType}", JavaExec) { 46 | description = "Run the application services and the required infrastructure services" 47 | classpath = sourceSets.endToEndTest.runtimeClasspath 48 | mainClass = "io.eventuate.examples.tram.sagas.customersandorders.application.CustomersAndOrdersMain" 49 | systemProperty "logback.debug", "true" 50 | systemProperty "eventuate.servicecontainer.serviceimage.version", version 51 | systemProperty "server.port", "0" 52 | systemProperty "spring.profiles.active", springProfilesActive 53 | dependsOn(":customer-service:customer-service-main:assemble", ":order-service:order-service-main:assemble", ":api-gateway-service:api-gateway-service-main:assemble") 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/application/CustomersAndOrdersMain.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.application; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.ApplicationUnderTest; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.context.event.ApplicationReadyEvent; 8 | import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; 9 | import org.springframework.context.event.EventListener; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.Model; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | 14 | @SpringBootApplication(excludeName = "org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration") 15 | @Controller 16 | public class CustomersAndOrdersMain { 17 | 18 | 19 | private static final ApplicationUnderTest application =ApplicationUnderTest.make(); // 20 | 21 | 22 | @RequestMapping(path = "/") 23 | public String index(Model model) { 24 | 25 | int apigatewayPort = application.getApigatewayPort(); 26 | int customerServicePort = application.getCustomerServicePort(); 27 | int orderServicePort = application.getOrderServicePort(); 28 | 29 | model.addAttribute("apiGatewayUrl", "http://localhost:" + apigatewayPort); 30 | model.addAttribute("customerServiceUrl", "http://localhost:" + customerServicePort); 31 | model.addAttribute("orderServiceUrl", "http://localhost:" + orderServicePort); 32 | 33 | return "index"; 34 | 35 | } 36 | 37 | @Autowired 38 | private ServletWebServerApplicationContext webServerAppCtxt; 39 | 40 | @EventListener(ApplicationReadyEvent.class) 41 | public void applicationReady() { 42 | 43 | System.out.printf(""" 44 | 45 | 46 | Visit http://localhost:%s/ for more information 47 | 48 | 49 | """, webServerAppCtxt.getWebServer().getPort()); 50 | } 51 | 52 | public static void main(String[] args) { 53 | 54 | application.start(); 55 | 56 | SpringApplication.run(CustomersAndOrdersMain.class, args); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/ApplicationUnderTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.util.ClassUtils; 6 | 7 | import java.lang.reflect.InvocationTargetException; 8 | 9 | public abstract class ApplicationUnderTest { 10 | 11 | protected Logger logger = LoggerFactory.getLogger(getClass()); 12 | 13 | public static ApplicationUnderTest make() { 14 | try { 15 | String className = ApplicationUnderTest.class.getName() + "Using" + System.getProperty("endToEndTestMode", "TestContainers"); 16 | Class clazz = ClassUtils.forName(className, ApplicationUnderTest.class.getClassLoader()); 17 | return (ApplicationUnderTest) clazz. getDeclaredConstructor().newInstance(); 18 | } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | 19 | InvocationTargetException e) { 20 | throw new RuntimeException(e); 21 | } 22 | } 23 | 24 | public abstract void start(); 25 | 26 | 27 | public String apiGatewayBaseUrl(String hostName, String path, String... pathElements) { 28 | return BaseUrlUtils.baseUrl(hostName, path, getApigatewayPort(), pathElements); 29 | } 30 | public abstract int getApigatewayPort(); 31 | public abstract int getCustomerServicePort(); 32 | public abstract int getOrderServicePort(); 33 | abstract boolean exposesSwaggerUiForBackendServices(); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/ApplicationUnderTestUsingDockerCompose.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests; 2 | 3 | public class ApplicationUnderTestUsingDockerCompose extends ApplicationUnderTest { 4 | @Override 5 | public void start() { 6 | // Do nothing 7 | } 8 | 9 | @Override 10 | public int getCustomerServicePort() { 11 | return 8081; 12 | } 13 | 14 | @Override 15 | public int getApigatewayPort() { 16 | return 8083; 17 | } 18 | 19 | @Override 20 | public int getOrderServicePort() { 21 | return 8081; 22 | } 23 | 24 | @Override 25 | boolean exposesSwaggerUiForBackendServices() { 26 | return true; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/ApplicationUnderTestUsingKind.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests; 2 | 3 | public class ApplicationUnderTestUsingKind extends ApplicationUnderTest { 4 | @Override 5 | public void start() { 6 | 7 | } 8 | 9 | @Override 10 | public int getApigatewayPort() { 11 | return 80; 12 | } 13 | 14 | @Override 15 | public int getCustomerServicePort() { 16 | throw new UnsupportedOperationException(); 17 | } 18 | 19 | @Override 20 | public int getOrderServicePort() { 21 | throw new UnsupportedOperationException(); 22 | } 23 | 24 | @Override 25 | boolean exposesSwaggerUiForBackendServices() { 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/ApplicationUnderTestUsingStubbed.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests; 2 | 3 | public class ApplicationUnderTestUsingStubbed extends ApplicationUnderTest { 4 | @Override 5 | public void start() { 6 | 7 | } 8 | 9 | @Override 10 | public int getApigatewayPort() { 11 | return 8081; 12 | } 13 | 14 | @Override 15 | public int getCustomerServicePort() { 16 | return 8082; 17 | } 18 | 19 | @Override 20 | public int getOrderServicePort() { 21 | return 8083; 22 | } 23 | 24 | @Override 25 | boolean exposesSwaggerUiForBackendServices() { 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/BaseUrlUtils.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertNotNull; 6 | 7 | public class BaseUrlUtils { 8 | @NotNull 9 | public static String baseUrl(String hostName, String path, int port, String[] pathElements) { 10 | assertNotNull(hostName, "host"); 11 | 12 | StringBuilder sb = new StringBuilder("http://"); 13 | sb.append(hostName); 14 | sb.append(":"); 15 | sb.append(port); 16 | sb.append("/"); 17 | sb.append(path); 18 | 19 | for (String pe : pathElements) { 20 | sb.append("/"); 21 | sb.append(pe); 22 | } 23 | return sb.toString(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/CustomersAndOrdersEndToEndTestConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.client.RestTemplate; 6 | 7 | @Configuration 8 | public class CustomersAndOrdersEndToEndTestConfiguration { 9 | 10 | @Bean 11 | public RestTemplate restTemplate() { 12 | return new RestTemplate(); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/proxies/apigateway/GetCustomerHistoryResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.apigateway; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | import io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.orderservice.GetOrderResponse; 5 | 6 | import java.util.List; 7 | 8 | public record GetCustomerHistoryResponse(Long customerId, String name, Money creditLimit, List orders) { 9 | public Money getCreditLimit() { 10 | return creditLimit(); 11 | } 12 | 13 | public Long getCustomerId() { 14 | return customerId(); 15 | } 16 | 17 | public String getName() { 18 | return name(); 19 | } 20 | 21 | public List getOrders() { 22 | return orders(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/proxies/customerservice/CreateCustomerRequest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.customerservice; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | 5 | public record CreateCustomerRequest(String name, Money creditLimit) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/proxies/customerservice/CreateCustomerResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.customerservice; 2 | 3 | 4 | public record CreateCustomerResponse(Long customerId) { 5 | } 6 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/proxies/customerservice/GetCustomerResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.customerservice; 2 | 3 | 4 | import io.eventuate.examples.common.money.Money; 5 | 6 | public record GetCustomerResponse(Long customerId, String name, Money creditLimit) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/proxies/customerservice/GetCustomersResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.customerservice; 2 | 3 | import java.util.List; 4 | 5 | public record GetCustomersResponse(List customers) { 6 | } 7 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/proxies/orderservice/CreateOrderRequest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.orderservice; 2 | 3 | 4 | import io.eventuate.examples.common.money.Money; 5 | 6 | public record CreateOrderRequest(Long customerId, Money orderTotal) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/proxies/orderservice/CreateOrderResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.orderservice; 2 | 3 | 4 | public record CreateOrderResponse(long orderId) { 5 | 6 | public Long getOrderId() { 7 | return orderId(); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/proxies/orderservice/GetOrderResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.orderservice; 2 | 3 | 4 | public record GetOrderResponse(Long orderId, OrderState orderState, RejectionReason rejectionReason) { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/proxies/orderservice/GetOrdersResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.orderservice; 2 | 3 | import java.util.List; 4 | 5 | public record GetOrdersResponse(List orders) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/proxies/orderservice/OrderState.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.orderservice; 2 | 3 | public enum OrderState { PENDING, APPROVED, REJECTED } 4 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/java/io/eventuate/examples/tram/sagas/customersandorders/endtoendtests/proxies/orderservice/RejectionReason.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.endtoendtests.proxies.orderservice; 2 | 3 | public enum RejectionReason { INSUFFICIENT_CREDIT, UNKNOWN_CUSTOMER} 4 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/resources/application.properties: -------------------------------------------------------------------------------- 1 | host.name=${DOCKER_HOST_IP:localhost} 2 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ERROR 5 | 6 | 7 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 8 | 9 | 10 | 11 | 12 | build/application.log 13 | true 14 | 15 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /end-to-end-tests/src/endToEndTest/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Customers and Orders 5 | 6 | 7 |

Welcome to Customers and Orders Application

8 | 9 |

Swagger UI

10 | 11 | 19 | 20 |

Async API UI

21 | 29 | 30 |

API endpoints

31 |
    32 |
  • 33 | API Gateway:  34 |
  • 35 |
  • 36 | Customer Service:  37 |
  • 38 |
  • 39 | Order Service:  40 |
  • 41 |
42 | 43 |

Using curl

44 | 45 | Use curl to: 46 | 47 |
    48 |
  1. Create a customer
  2. 49 |
  3. Create an order
  4. 50 |
  5. Get an order
  6. 51 |
52 | 53 |

Step 1: Create a Customer

54 | 55 |

56 | curl -X POST --header "Content-Type: application/json" -d '{
57 |   "creditLimit": {
58 |     "amount": 5
59 |   },
60 |   "name": "Jane Doe"
61 | }' [[${apiGatewayUrl}]]/customers
62 | 
63 | 64 |

Step 2: Create an Order

65 | 66 |

67 | curl -X POST --header "Content-Type: application/json" -d '{
68 |   "customerId": 1,
69 |   "orderTotal": {
70 |     "amount": 4
71 |   }
72 | }' [[${apiGatewayUrl}]]/orders
73 | 
74 | 75 |

Step 3: Get Order (status)

76 | 77 |

78 | curl -X GET [[${apiGatewayUrl}]]/orders/1
79 | 
80 | 81 |

Accessing the service databases

82 | 83 | There are the following scripts: 84 | 85 |
    86 |
  • ./mysql-customer-service-cli.sh
  • 87 |
  • ./mysql-order-service-cli.sh
  • 88 |
  • ./postgres-customer-service-cli.sh
  • 89 |
  • ./postgres-order-service-cli.sh
  • 90 |
91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | deployUrl=file:///Users/cer/.m2/testdeploy 2 | 3 | eventuateMavenRepoUrl=file:///Users/cer/.m2/testdeploy,https://snapshots.repositories.eventuate.io/repository 4 | 5 | springBootVersion=3.4.0 6 | springCloudVersion=2024.0.0 7 | springCloudContractDependenciesVersion=4.2.0 8 | 9 | eventuatePlatformVersion=2025.0.RELEASE 10 | 11 | eventuateTramSpringTestingSupportCloudContractVersion=0.1.0.RELEASE 12 | 13 | springdocVersion=2.1.0 14 | 15 | eventuateCommonExamplesVersion=0.4.0.RELEASE 16 | 17 | eventuatePlatformTestContainerSupportVersion=0.4.0.RELEASE 18 | 19 | assertjVersion=3.23.1 20 | springwolfVersion=1.11.0 21 | 22 | version=0.1.0-SNAPSHOT 23 | -------------------------------------------------------------------------------- /gradle/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 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 %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 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 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eventuate-tram/eventuate-tram-sagas-examples-customers-and-orders/ffe523909be3f46049d2feadf28d7c257da68e96/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Aug 17 21:50:31 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-8.13-bin.zip 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 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 %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 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 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /images/Eventuate_Tram_Customer_and_Order_Orchestration_Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eventuate-tram/eventuate-tram-sagas-examples-customers-and-orders/ffe523909be3f46049d2feadf28d7c257da68e96/images/Eventuate_Tram_Customer_and_Order_Orchestration_Architecture.png -------------------------------------------------------------------------------- /images/Orchestration_flow.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eventuate-tram/eventuate-tram-sagas-examples-customers-and-orders/ffe523909be3f46049d2feadf28d7c257da68e96/images/Orchestration_flow.jpeg -------------------------------------------------------------------------------- /kafka-consumer-groups.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | docker run --network=${PWD##*/}_default --rm confluentinc/cp-kafka:5.2.4 sh -c "exec kafka-consumer-groups --bootstrap-server kafka:29092 $*" 4 | -------------------------------------------------------------------------------- /kafka-topics.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | docker run --network=${PWD##*/}_default --rm confluentinc/cp-kafka:5.2.4 sh -c "exec kafka-topics --bootstrap-server kafka:29092 $*" 4 | -------------------------------------------------------------------------------- /migration-tests/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | testImplementation "org.springframework.boot:spring-boot-starter-jdbc" 3 | testImplementation "org.springframework.boot:spring-boot-starter-test" 4 | 5 | testImplementation 'mysql:mysql-connector-java:8.0.21' 6 | testRuntimeOnly ('org.postgresql:postgresql:9.4-1200-jdbc41') { 7 | exclude group: "org.slf4j", module: "slf4j-simple" 8 | } 9 | testRuntimeOnly 'com.microsoft.sqlserver:mssql-jdbc:7.2.1.jre8' 10 | } 11 | 12 | test { 13 | if (!project.ext.has("verifyDbIdMigration")) { 14 | exclude '**/DbIdMigrationVerificationTest**' 15 | } 16 | 17 | forkEvery 1 18 | } 19 | -------------------------------------------------------------------------------- /migration-tests/src/test/java/io/eventuate/examples/tram/sagas/ordersandcustomers/migration/DbIdMigrationVerificationTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.migration; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.jdbc.core.JdbcTemplate; 11 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 12 | 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | @RunWith(SpringJUnit4ClassRunner.class) 17 | @SpringBootTest(classes = DbIdMigrationVerificationTest.Config.class) 18 | public class DbIdMigrationVerificationTest { 19 | 20 | @Configuration 21 | @EnableAutoConfiguration 22 | public static class Config {} 23 | 24 | @Autowired 25 | private JdbcTemplate jdbcTemplate; 26 | 27 | @Test 28 | //after first call of e2e tests (before migration), messages should have ids, after second call (after migration) don't 29 | public void testThatMessagesAreMigrated() { 30 | List> messagesWithEmptyId = 31 | jdbcTemplate.queryForList("select * from eventuate.message where destination <> 'CDC-IGNORED' and id = ''"); 32 | 33 | List> messagesWithNotEmptyId = 34 | jdbcTemplate.queryForList("select * from eventuate.message where destination <> 'CDC-IGNORED' and id <> ''"); 35 | 36 | Assert.assertTrue(messagesWithEmptyId.size() > 0); 37 | Assert.assertEquals(messagesWithEmptyId.size(), messagesWithNotEmptyId.size()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /migration-tests/src/test/resources/application-mssql.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:sqlserver://${DOCKER_HOST_IP:localhost}:1433;databaseName=eventuate 2 | spring.datasource.username=sa 3 | spring.datasource.password=Eventuate123! 4 | spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver -------------------------------------------------------------------------------- /migration-tests/src/test/resources/application-postgres.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://${DOCKER_HOST_IP:localhost}/eventuate 2 | spring.datasource.username=eventuate 3 | spring.datasource.password=eventuate 4 | spring.datasource.driver-class-name=org.postgresql.Driver -------------------------------------------------------------------------------- /migration-tests/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:mysql://${DOCKER_HOST_IP:localhost}/eventuate 2 | spring.datasource.username=mysqluser 3 | spring.datasource.password=mysqlpw 4 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 5 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | node = "22" 3 | -------------------------------------------------------------------------------- /mysql-customer-service-cli.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | IMAGE=$(docker ps --filter "network=CustomersAndOrdersEndToEndTest" --format "{{.Image}}" \ 4 | | grep mysql8 | head -1) 5 | 6 | docker run ${1:--it} --rm \ 7 | --network=CustomersAndOrdersEndToEndTest --rm \ 8 | "${IMAGE?}" \ 9 | sh -c 'exec mysql -hcustomer-service-db -uroot -prootpassword -o eventuate' 10 | -------------------------------------------------------------------------------- /mysql-order-service-cli.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | IMAGE=$(docker ps --filter "network=CustomersAndOrdersEndToEndTest" --format "{{.Image}}" \ 4 | | grep mysql8 | head -1) 5 | 6 | docker run ${1:--it} --rm \ 7 | --network=CustomersAndOrdersEndToEndTest --rm \ 8 | "${IMAGE?}" \ 9 | sh -c 'exec mysql -horder-service-db -uroot -prootpassword -o eventuate' 10 | -------------------------------------------------------------------------------- /order-service/order-service-domain/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | implementation "io.eventuate.examples.common:eventuate-examples-common-money-jakarta9:$eventuateCommonExamplesVersion" 4 | 5 | compileOnly "org.springframework.boot:spring-boot-starter-data-jpa" 6 | testImplementation "org.mockito:mockito-core" 7 | } 8 | -------------------------------------------------------------------------------- /order-service/order-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/domain/Order.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.domain; 2 | 3 | 4 | import jakarta.persistence.*; 5 | 6 | @Entity 7 | @Table(name="orders") 8 | @Access(AccessType.FIELD) 9 | public class Order { 10 | 11 | @Id 12 | @GeneratedValue(strategy = GenerationType.IDENTITY) 13 | private Long id; 14 | 15 | @Enumerated(EnumType.STRING) 16 | private OrderState state; 17 | 18 | @Embedded 19 | private OrderDetails orderDetails; 20 | 21 | @Enumerated(EnumType.STRING) 22 | private RejectionReason rejectionReason; 23 | 24 | @Version 25 | private Long version; 26 | 27 | public Order() { 28 | } 29 | 30 | public Order(OrderDetails orderDetails) { 31 | this.orderDetails = orderDetails; 32 | this.state = OrderState.PENDING; 33 | } 34 | 35 | public static Order createOrder(OrderDetails orderDetails) { 36 | return new Order(orderDetails); 37 | } 38 | 39 | public Long getId() { 40 | return id; 41 | } 42 | 43 | public void setId(long id) { 44 | this.id = id; 45 | } 46 | 47 | public OrderDetails getOrderDetails() { 48 | return orderDetails; 49 | } 50 | 51 | public void approve() { 52 | this.state = OrderState.APPROVED; 53 | } 54 | 55 | public void reject(RejectionReason rejectionReason) { 56 | this.state = OrderState.REJECTED; 57 | this.rejectionReason = rejectionReason; 58 | } 59 | 60 | public OrderState getState() { 61 | return state; 62 | } 63 | 64 | public RejectionReason getRejectionReason() { 65 | return rejectionReason; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /order-service/order-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/domain/OrderDetails.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.domain; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | import jakarta.persistence.Embeddable; 5 | import jakarta.persistence.Embedded; 6 | 7 | @Embeddable 8 | public record OrderDetails(Long customerId, @Embedded Money orderTotal) { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /order-service/order-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/domain/OrderDomainConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.domain; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | public class OrderDomainConfiguration { 8 | 9 | @Bean 10 | public OrderService orderService(OrderRepository orderRepository) { 11 | return new OrderService(orderRepository); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /order-service/order-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/domain/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.domain; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | import java.util.List; 6 | 7 | public interface OrderRepository extends CrudRepository { 8 | List findAllByOrderDetailsCustomerId(Long customerId); 9 | } 10 | -------------------------------------------------------------------------------- /order-service/order-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/domain/OrderService.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.domain; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | 5 | public class OrderService { 6 | 7 | @Autowired 8 | private OrderRepository orderRepository; 9 | 10 | public OrderService(OrderRepository orderRepository) { 11 | this.orderRepository = orderRepository; 12 | } 13 | 14 | public Order createOrder(OrderDetails orderDetails) { 15 | Order order = Order.createOrder(orderDetails); 16 | orderRepository.save(order); 17 | return order; 18 | } 19 | 20 | public void approveOrder(Long orderId) { 21 | orderRepository.findById(orderId).get().approve(); 22 | } 23 | 24 | public void rejectOrder(Long orderId, RejectionReason rejectionReason) { 25 | orderRepository.findById(orderId).get().reject(rejectionReason); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /order-service/order-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/domain/OrderState.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.domain; 2 | 3 | public enum OrderState { PENDING, APPROVED, REJECTED } 4 | -------------------------------------------------------------------------------- /order-service/order-service-domain/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/domain/RejectionReason.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.domain; 2 | 3 | public enum RejectionReason { INSUFFICIENT_CREDIT, UNKNOWN_CUSTOMER} 4 | -------------------------------------------------------------------------------- /order-service/order-service-main/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG baseImageVersion 2 | FROM eventuateio/eventuate-examples-docker-images-spring-example-base-image:$baseImageVersion 3 | ARG serviceImageVersion 4 | COPY build/libs/order-service-main-$serviceImageVersion.jar service.jar 5 | -------------------------------------------------------------------------------- /order-service/order-service-main/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: ServicePlugin 2 | 3 | dependencies { 4 | implementation project(":order-service:order-service-restapi") 5 | implementation project(":order-service:order-service-sagas") 6 | implementation project(":order-service:order-service-persistence") 7 | implementation "io.eventuate.tram.core:eventuate-tram-spring-logging" 8 | 9 | implementation "org.springframework.boot:spring-boot-starter-actuator" 10 | implementation "org.springframework.boot:spring-boot-starter-web" 11 | 12 | testImplementation "org.mockito:mockito-core" 13 | 14 | componentTestImplementation project(":order-service:order-service-domain") 15 | componentTestImplementation project(":order-service:order-service-proxies-customer-service") 16 | 17 | componentTestImplementation "io.eventuate.platform.testcontainer.support:eventuate-platform-testcontainer-support-service:$eventuatePlatformTestContainerSupportVersion" 18 | 19 | componentTestImplementation "io.eventuate.common:eventuate-common-testcontainers" 20 | componentTestImplementation "io.eventuate.messaging.kafka:eventuate-messaging-kafka-testcontainers" 21 | componentTestImplementation "io.eventuate.cdc:eventuate-cdc-testcontainers" 22 | componentTestImplementation "io.rest-assured:rest-assured" 23 | componentTestImplementation "io.eventuate.tram.core:eventuate-tram-spring-in-memory" 24 | componentTestImplementation "io.eventuate.tram.springwolf:eventuate-tram-springwolf-support-testing" 25 | 26 | } 27 | 28 | check.shouldRunAfter(":order-service:order-service-restapi:check") 29 | check.shouldRunAfter(":order-service:order-service-persistence:check") 30 | check.shouldRunAfter(":order-service:order-service-sagas:check") 31 | -------------------------------------------------------------------------------- /order-service/order-service-main/src/componentTest/java/io/eventuate/examples/tram/sagas/customersandorders/orders/OrderServiceInProcessComponentTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.commands.ReserveCreditCommand; 4 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditLimitExceeded; 5 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditReserved; 6 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerNotFound; 7 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderDomainConfiguration; 8 | import io.eventuate.examples.tram.sagas.customersandorders.orders.persistence.OrderPersistenceConfiguration; 9 | import io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers.CustomerServiceProxyConfiguration; 10 | import io.eventuate.examples.tram.sagas.customersandorders.orders.restapi.OrderRestApiConfiguration; 11 | import io.eventuate.examples.tram.sagas.customersandorders.orders.sagas.CreateOrderSaga; 12 | import io.eventuate.examples.tram.sagas.customersandorders.orders.sagas.OrderSagasConfiguration; 13 | import io.eventuate.tram.spring.inmemory.TramInMemoryConfiguration; 14 | import io.eventuate.tram.spring.springwolf.testing.AsyncApiDocument; 15 | import io.restassured.RestAssured; 16 | import org.junit.jupiter.api.BeforeEach; 17 | import org.junit.jupiter.api.Test; 18 | import org.slf4j.Logger; 19 | import org.slf4j.LoggerFactory; 20 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 21 | import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; 22 | import org.springframework.boot.test.context.SpringBootTest; 23 | import org.springframework.boot.test.web.server.LocalServerPort; 24 | import org.springframework.context.annotation.Configuration; 25 | import org.springframework.context.annotation.Import; 26 | 27 | import java.util.List; 28 | 29 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 30 | public class OrderServiceInProcessComponentTest { 31 | 32 | protected static Logger logger = LoggerFactory.getLogger(OrderServiceInProcessComponentTest.class); 33 | 34 | @Configuration 35 | @EnableAutoConfiguration(exclude = FlywayAutoConfiguration.class) 36 | @Import({OrderRestApiConfiguration.class, 37 | OrderPersistenceConfiguration.class, 38 | OrderDomainConfiguration.class, 39 | CustomerServiceProxyConfiguration.class, 40 | OrderSagasConfiguration.class, 41 | TramInMemoryConfiguration.class 42 | }) 43 | static public class Config { 44 | 45 | } 46 | 47 | @LocalServerPort 48 | private int port; 49 | 50 | @BeforeEach 51 | public void setup() { 52 | RestAssured.port = port; 53 | } 54 | 55 | @Test 56 | void shouldExposeSwaggerUI() { 57 | RestAssured.given() 58 | .get("/swagger-ui/index.html") 59 | .then() 60 | .statusCode(200); 61 | } 62 | 63 | @Test 64 | public void shouldExposeSpringWolf() { 65 | AsyncApiDocument doc = AsyncApiDocument.getSpringWolfDoc(); 66 | 67 | String createOrderSaga = CreateOrderSaga.class.getName(); 68 | 69 | doc.assertSendsMessage(createOrderSaga + "-customerService-reserveCredit", 70 | "customerService", 71 | ReserveCreditCommand.class.getName()); 72 | 73 | for (var replyClass : List.of(CustomerCreditReserved.class, CustomerNotFound.class, CustomerCreditLimitExceeded.class)) { 74 | doc.assertReceivesMessage("receive-" + createOrderSaga + "-reply", 75 | createOrderSaga + "-reply", 76 | replyClass.getName()); 77 | } 78 | 79 | } 80 | 81 | @Test 82 | public void shouldExposeSpringWolfUi() { 83 | 84 | RestAssured.given() 85 | .get("/springwolf/asyncapi-ui.html") 86 | .then() 87 | .statusCode(200); 88 | 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /order-service/order-service-main/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/OrderServiceMain.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.web.filter.OncePerRequestFilter; 9 | 10 | import jakarta.servlet.FilterChain; 11 | import jakarta.servlet.ServletException; 12 | import jakarta.servlet.http.HttpServletRequest; 13 | import jakarta.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | 16 | @SpringBootApplication 17 | public class OrderServiceMain { 18 | 19 | private Logger _logger = LoggerFactory.getLogger(getClass()); 20 | 21 | 22 | @Bean 23 | public OncePerRequestFilter logFilter() { 24 | return new OncePerRequestFilter() { 25 | @Override 26 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 27 | _logger.info("Path: {}", request.getRequestURI()); 28 | filterChain.doFilter(request, response); 29 | _logger.info("Path: {} {}", request.getRequestURI(), response.getStatus()); 30 | } 31 | }; 32 | } 33 | 34 | 35 | public static void main(String[] args) { 36 | SpringApplication.run(OrderServiceMain.class, args); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /order-service/order-service-main/src/main/resources/application-postgres.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://${DOCKER_HOST_IP:localhost}/eventuate 2 | spring.datasource.username=eventuate 3 | spring.datasource.password=eventuate 4 | spring.datasource.driver-class-name=org.postgresql.Driver 5 | 6 | spring.flyway.locations=classpath:flyway/{vendor} 7 | spring.flyway.baseline-on-migrate=true 8 | spring.flyway.baseline-version=0 9 | 10 | -------------------------------------------------------------------------------- /order-service/order-service-main/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=order-service 2 | spring.jpa.generate-ddl=true 3 | logging.level.org.springframework.orm.jpa=INFO 4 | logging.level.org.hibernate.SQL=DEBUG 5 | logging.level.io.eventuate=DEBUG 6 | logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG 7 | logging.level.org.springframework.boot.autoconfigure=DEBUG 8 | 9 | eventuatelocal.kafka.bootstrap.servers=${DOCKER_HOST_IP:localhost}:9092 10 | eventuatelocal.zookeeper.connection.string=${DOCKER_HOST_IP:localhost}:2181 11 | 12 | spring.datasource.url=jdbc:mysql://${DOCKER_HOST_IP:localhost}/order_service 13 | spring.datasource.username=mysqluser 14 | spring.datasource.password=mysqlpw 15 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 16 | 17 | management.tracing.enabled=true 18 | management.tracing.sampling.probability=1 19 | spring.zipkin.base.url=http://${DOCKER_HOST_IP:localhost}:9411/ 20 | 21 | # Copy/paste 22 | 23 | spring.flyway.locations=classpath:flyway/{vendor} 24 | spring.flyway.baseline-on-migrate=true 25 | spring.flyway.baseline-version=0 26 | 27 | # SpringWolf 28 | 29 | springwolf.docket.base-package=io.eventuate.examples.tram.ordersandcustomers 30 | 31 | springwolf.docket.info.title=${spring.application.name} 32 | springwolf.docket.info.version=1.0.0 33 | springwolf.docket.scanner.async-listener.enabled=false 34 | 35 | springwolf.docket.servers.eventuate-producer.protocol=eventuate-outbox 36 | springwolf.docket.servers.eventuate-producer.host=${spring.datasource.url} 37 | springwolf.docket.servers.eventuate-consumer.protocol=kafka 38 | springwolf.docket.servers.eventuate-consumer.host=${eventuatelocal.kafka.bootstrap.servers} 39 | -------------------------------------------------------------------------------- /order-service/order-service-persistence/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: IntegrationTestsPlugin 2 | 3 | dependencies { 4 | implementation project(":order-service:order-service-domain") 5 | 6 | 7 | implementation "io.eventuate.common:eventuate-common-jdbc" 8 | 9 | implementation "org.springframework.boot:spring-boot-starter-data-jpa" 10 | 11 | testImplementation "org.mockito:mockito-core" 12 | 13 | implementation 'com.mysql:mysql-connector-j:8.0.33' 14 | implementation 'org.postgresql:postgresql:9.4-1200-jdbc41' 15 | 16 | 17 | integrationTestImplementation "io.eventuate.examples.common:eventuate-examples-common-money-jakarta9:$eventuateCommonExamplesVersion" 18 | integrationTestImplementation "io.eventuate.common:eventuate-common-testcontainers" 19 | 20 | } 21 | 22 | -------------------------------------------------------------------------------- /order-service/order-service-persistence/src/integrationTest/java/io/eventuate/examples/tram/sagas/customersandorders/orders/persistence/OrderServiceRepositoriesTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.persistence; 2 | 3 | import io.eventuate.common.testcontainers.ContainerTestUtil; 4 | import io.eventuate.common.testcontainers.DatabaseContainerFactory; 5 | import io.eventuate.common.testcontainers.EventuateDatabaseContainer; 6 | import io.eventuate.common.testcontainers.PropertyProvidingContainer; 7 | import io.eventuate.examples.common.money.Money; 8 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.Order; 9 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderDetails; 10 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderRepository; 11 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderState; 12 | import org.junit.jupiter.api.Test; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; 15 | import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; 16 | import org.springframework.context.annotation.Configuration; 17 | import org.springframework.context.annotation.Import; 18 | import org.springframework.test.context.ContextConfiguration; 19 | import org.springframework.test.context.DynamicPropertyRegistry; 20 | import org.springframework.test.context.DynamicPropertySource; 21 | import org.springframework.transaction.annotation.Propagation; 22 | import org.springframework.transaction.annotation.Transactional; 23 | import org.springframework.transaction.support.TransactionTemplate; 24 | 25 | import static org.junit.jupiter.api.Assertions.assertEquals; 26 | 27 | @DataJpaTest 28 | @ContextConfiguration(classes= OrderServiceRepositoriesTest.Config.class) 29 | @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) 30 | @Transactional(propagation = Propagation.NEVER) 31 | public class OrderServiceRepositoriesTest { 32 | 33 | @Configuration 34 | @Import(OrderPersistenceConfiguration.class) 35 | static public class Config { 36 | } 37 | 38 | public static EventuateDatabaseContainer database = 39 | DatabaseContainerFactory.makeVanillaDatabaseContainer() 40 | .withReuse(ContainerTestUtil.shouldReuse()); 41 | 42 | @DynamicPropertySource 43 | static void registerMySqlProperties(DynamicPropertyRegistry registry) { 44 | PropertyProvidingContainer.startAndProvideProperties(registry, database); 45 | } 46 | 47 | @Autowired 48 | private OrderRepository orderRepository; 49 | 50 | @Autowired 51 | private TransactionTemplate transactionTemplate; 52 | 53 | 54 | @Test 55 | public void shouldSaveAndLoadOrder() { 56 | 57 | long customerId = 123L; 58 | Money orderTotal = new Money("12.34"); 59 | OrderDetails orderDetails = new OrderDetails(customerId, orderTotal); 60 | 61 | Order o = new Order(orderDetails); 62 | 63 | transactionTemplate.executeWithoutResult( ts -> orderRepository.save(o) ); 64 | 65 | long orderId = o.getId(); 66 | 67 | transactionTemplate.executeWithoutResult(ts -> { 68 | Order o2 = orderRepository.findById(orderId).get(); 69 | assertEquals(orderDetails, o2.getOrderDetails()); 70 | assertEquals(OrderState.PENDING, o2.getState()); 71 | 72 | o2.approve(); 73 | }); 74 | 75 | transactionTemplate.executeWithoutResult(ts -> { 76 | Order o2 = orderRepository.findById(orderId).get(); 77 | assertEquals(OrderState.APPROVED, o2.getState()); 78 | }); 79 | 80 | 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /order-service/order-service-persistence/src/integrationTest/resources/application-postgres.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://${DOCKER_HOST_IP:localhost}/eventuate 2 | spring.datasource.username=eventuate 3 | spring.datasource.password=eventuate 4 | spring.datasource.driver-class-name=org.postgresql.Driver -------------------------------------------------------------------------------- /order-service/order-service-persistence/src/integrationTest/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=order-service 2 | spring.jpa.generate-ddl=true 3 | logging.level.org.springframework.orm.jpa=INFO 4 | logging.level.org.hibernate.SQL=DEBUG 5 | logging.level.io.eventuate=DEBUG 6 | logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG 7 | 8 | eventuatelocal.kafka.bootstrap.servers=${DOCKER_HOST_IP:localhost}:9092 9 | eventuatelocal.zookeeper.connection.string=${DOCKER_HOST_IP:localhost}:2181 10 | 11 | spring.datasource.url=jdbc:mysql://${DOCKER_HOST_IP:localhost}/customer_service 12 | spring.datasource.username=mysqluser 13 | spring.datasource.password=mysqlpw 14 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 15 | 16 | management.tracing.enabled=true 17 | management.tracing.sampling.probability=1 18 | spring.zipkin.base.url=http://${DOCKER_HOST_IP:localhost}:9411/ 19 | 20 | spring.flyway.locations=classpath:flyway/{vendor} 21 | spring.flyway.baseline-on-migrate=true 22 | spring.flyway.baseline-version=0 23 | -------------------------------------------------------------------------------- /order-service/order-service-persistence/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/persistence/OrderPersistenceConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.persistence; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.Order; 4 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderRepository; 5 | import org.springframework.boot.autoconfigure.domain.EntityScan; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 8 | 9 | @Configuration 10 | @EnableJpaRepositories(basePackageClasses = {OrderRepository.class}) 11 | @EntityScan(basePackageClasses = {Order.class}) 12 | public class OrderPersistenceConfiguration { 13 | } 14 | -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'org.springframework.cloud.contract' 2 | 3 | dependencies { 4 | implementation "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-orchestration-simple-dsl-starter" 5 | implementation "io.eventuate.examples.common:eventuate-examples-common-money-jakarta9:$eventuateCommonExamplesVersion" 6 | 7 | contractTestImplementation "io.eventuate.tram.testingsupport.springcloudcontract:eventuate-tram-spring-testing-support-cloud-contract:$eventuateTramSpringTestingSupportCloudContractVersion" 8 | contractTestImplementation "org.springframework.cloud:spring-cloud-starter-contract-stub-runner" 9 | 10 | } 11 | 12 | contractTest { 13 | systemProperty "stubrunner.repositoryRoot", contractRepoUrl 14 | 15 | } 16 | 17 | ext { 18 | set('contractGroupId', project.group) 19 | set('contractArtifactId', "customer-service-credit-reservation-api") 20 | set('contractArtifactPath', "customer-service:$contractArtifactId") 21 | set("contractArtifactVersion", project.version) 22 | } 23 | 24 | contractTest.dependsOn(":$contractArtifactPath:publishStubsPublicationToStubsRepository") 25 | 26 | 27 | contracts { 28 | 29 | contractDependency { 30 | groupId = contractGroupId 31 | artifactId = contractArtifactId 32 | classifier = "stubs" 33 | version = contractArtifactVersion 34 | } 35 | 36 | contractsMode = "REMOTE" 37 | 38 | contractRepository { 39 | repositoryUrl = contractRepoUrl 40 | } 41 | 42 | contractsPath = "META-INF/${contractGroupId}/$contractArtifactId/${contractArtifactVersion}/contracts/order-service/commands" 43 | 44 | testFramework = "JUNIT5" 45 | baseClassForTests = "io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers.BaseForCustomerServiceTest" 46 | 47 | failOnNoContracts = true 48 | 49 | } 50 | 51 | copyContracts.dependsOn(":$contractArtifactPath:publishStubsPublicationToStubsRepository") 52 | 53 | -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/contractTest/java/io/eventuate/examples/tram/sagas/customersandorders/orders/proxies/customers/BaseForCustomerServiceTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers; 2 | 3 | import io.eventuate.common.json.mapper.JSonMapper; 4 | import io.eventuate.examples.common.money.Money; 5 | import io.eventuate.tram.commands.producer.CommandProducer; 6 | import io.eventuate.tram.spring.testing.cloudcontract.EnableEventuateTramContractVerifier; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Import; 12 | 13 | import java.util.Collections; 14 | 15 | 16 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = {BaseForCustomerServiceTest.TestConfig.class}) 17 | public abstract class BaseForCustomerServiceTest { 18 | 19 | static { 20 | // JSonMapper.objectMapper.findAndRegisterModules() - seems useful 21 | JSonMapper.objectMapper.registerModule(new MoneyModule()); 22 | } 23 | 24 | @Configuration 25 | @EnableAutoConfiguration 26 | @EnableEventuateTramContractVerifier 27 | @Import({CustomerServiceProxyConfiguration.class}) 28 | public static class TestConfig { 29 | 30 | } 31 | 32 | @Autowired 33 | private CustomerServiceProxy customerServiceProxy; 34 | 35 | @Autowired 36 | private CommandProducer commandProducer; 37 | 38 | protected void reserveCredit() { 39 | var command = customerServiceProxy.reserveCredit(102, 101L, new Money(103)); 40 | String replyTo = "reserveCreditReply"; 41 | commandProducer.send(command.getDestinationChannel(), command.getCommand(), replyTo, Collections.emptyMap()); 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/contractTest/java/io/eventuate/examples/tram/sagas/customersandorders/orders/proxies/customers/MoneyModule.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers; 2 | 3 | import com.fasterxml.jackson.core.JsonGenerator; 4 | import com.fasterxml.jackson.core.JsonParser; 5 | import com.fasterxml.jackson.core.JsonProcessingException; 6 | import com.fasterxml.jackson.core.JsonToken; 7 | import com.fasterxml.jackson.databind.DeserializationContext; 8 | import com.fasterxml.jackson.databind.SerializerProvider; 9 | import com.fasterxml.jackson.databind.deser.std.StdScalarDeserializer; 10 | import com.fasterxml.jackson.databind.module.SimpleModule; 11 | import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer; 12 | import io.eventuate.examples.common.money.Money; 13 | 14 | import java.io.IOException; 15 | 16 | public class MoneyModule extends SimpleModule { 17 | 18 | class MoneyDeserializer extends StdScalarDeserializer { 19 | 20 | protected MoneyDeserializer() { 21 | super(Money.class); 22 | } 23 | 24 | @Override 25 | public Money deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { 26 | JsonToken token = jp.getCurrentToken(); 27 | if (token == JsonToken.VALUE_STRING) { 28 | String str = jp.getText().trim(); 29 | if (str.isEmpty()) 30 | return null; 31 | else 32 | return new Money(str); 33 | } else 34 | throw ctxt.wrongTokenException(jp, getValueClass(), JsonToken.VALUE_STRING, "Expected JSON String"); 35 | } 36 | } 37 | 38 | class MoneySerializer extends StdScalarSerializer { 39 | public MoneySerializer() { 40 | super(Money.class); 41 | } 42 | 43 | @Override 44 | public void serialize(Money value, JsonGenerator jgen, SerializerProvider provider) throws IOException { 45 | jgen.writeString(value.getAmount().toString()); 46 | } 47 | } 48 | 49 | @Override 50 | public String getModuleName() { 51 | return "MoneyModule"; 52 | } 53 | 54 | public MoneyModule() { 55 | addDeserializer(Money.class, new MoneyDeserializer()); 56 | addSerializer(Money.class, new MoneySerializer()); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/contractTest/java/io/eventuate/examples/tram/sagas/customersandorders/orders/proxies/customers/ReplyHandlersTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditReserved; 4 | import io.eventuate.tram.messaging.consumer.MessageConsumer; 5 | import io.eventuate.tram.spring.testing.cloudcontract.EnableEventuateTramContractVerifier; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.cloud.contract.stubrunner.StubFinder; 11 | import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; 12 | import org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.test.annotation.DirtiesContext; 16 | 17 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) 18 | @AutoConfigureStubRunner(ids = "io.eventuate.examples.tram.sagas.customersandorders:customer-service-credit-reservation-api:+", 19 | stubsMode = StubRunnerProperties.StubsMode.REMOTE 20 | ) 21 | @DirtiesContext 22 | public class ReplyHandlersTest { 23 | 24 | @Configuration 25 | @EnableAutoConfiguration 26 | @EnableEventuateTramContractVerifier 27 | public static class TestConfiguration { 28 | 29 | @Bean 30 | public TestReplyConsumer testMessageConsumer(MessageConsumer messageConsumer) { 31 | return new TestReplyConsumer(ReplyHandlersTest.class.getName(), "reserveCreditReply", messageConsumer); 32 | } 33 | 34 | } 35 | 36 | @Autowired 37 | private StubFinder stubFinder; 38 | 39 | @Autowired 40 | private TestReplyConsumer testReplyConsumer; 41 | 42 | 43 | @Test 44 | public void shouldHandleCreditReservedReply() { 45 | stubFinder.trigger("creditReserved"); 46 | 47 | testReplyConsumer.assertReplyOfTypeReceived(CustomerCreditReserved.class); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/contractTest/java/io/eventuate/examples/tram/sagas/customersandorders/orders/proxies/customers/TestReplyConsumer.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers; 2 | 3 | import io.eventuate.common.json.mapper.JSonMapper; 4 | import io.eventuate.tram.commands.common.ReplyMessageHeaders; 5 | import io.eventuate.tram.messaging.common.Message; 6 | import io.eventuate.tram.messaging.consumer.MessageConsumer; 7 | import org.junit.jupiter.api.Assertions; 8 | 9 | import javax.annotation.PostConstruct; 10 | import java.util.Set; 11 | import java.util.concurrent.BlockingQueue; 12 | import java.util.concurrent.LinkedBlockingQueue; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | public class TestReplyConsumer { 16 | 17 | private static BlockingQueue messages = new LinkedBlockingQueue<>(); 18 | 19 | private final String subscriberId; 20 | private final MessageConsumer messageConsumer; 21 | private final String channel; 22 | 23 | public TestReplyConsumer(String subscriberId, String channel, MessageConsumer messageConsumer) { 24 | this.subscriberId = subscriberId; 25 | this.messageConsumer = messageConsumer; 26 | this.channel = channel; 27 | } 28 | 29 | @PostConstruct 30 | private void subscriber() { 31 | messageConsumer.subscribe(subscriberId, Set.of(channel), message -> { 32 | messages.add(message); 33 | }); 34 | } 35 | 36 | public Message assertMessageReceived() { 37 | try { 38 | return messages.poll(2, TimeUnit.SECONDS); 39 | } catch (InterruptedException e) { 40 | throw new RuntimeException(e); 41 | } 42 | } 43 | 44 | void assertReplyOfTypeReceived(Class expectedReplyType) { 45 | var message = assertMessageReceived(); 46 | String replyType = message.getRequiredHeader(ReplyMessageHeaders.REPLY_TYPE); 47 | Assertions.assertEquals(expectedReplyType.getName(), replyType); 48 | var replyObject = JSonMapper.fromJsonByName(message.getPayload(), replyType); 49 | Assertions.assertNotNull(replyObject); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/contractTest/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.datasource.url=jdbc:postgresql://${DOCKER_HOST_IP:localhost}/eventuate 2 | spring.datasource.username=eventuate 3 | spring.datasource.password=eventuate 4 | spring.datasource.driver-class-name=org.postgresql.Driver 5 | spring.datasource.hikari.initialization-fail-timeout=30000 6 | 7 | eventuatelocal.kafka.bootstrap.servers=${DOCKER_HOST_IP:localhost}:9092 8 | -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/commands/ReserveCreditCommand.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.commands; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | import io.eventuate.tram.commands.common.Command; 5 | 6 | public record ReserveCreditCommand(Long customerId, Long orderId, Money orderTotal) implements Command { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/replies/CustomerCreditLimitExceeded.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies; 2 | 3 | public class CustomerCreditLimitExceeded implements ReserveCreditResult { 4 | } 5 | -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/replies/CustomerCreditReserved.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies; 2 | 3 | public class CustomerCreditReserved implements ReserveCreditResult { 4 | } 5 | -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/replies/CustomerNotFound.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies; 2 | 3 | public class CustomerNotFound implements ReserveCreditResult { 4 | } 5 | -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/customers/creditreservationapi/replies/ReserveCreditResult.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies; 2 | 3 | public interface ReserveCreditResult { 4 | } 5 | -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/proxies/customers/CustomerServiceProxy.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.commands.ReserveCreditCommand; 5 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditLimitExceeded; 6 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerNotFound; 7 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.ReserveCreditResult; 8 | import io.eventuate.tram.commands.consumer.CommandWithDestination; 9 | import io.eventuate.tram.commands.consumer.CommandWithDestinationBuilder; 10 | import io.eventuate.tram.sagas.simpledsl.annotations.SagaParticipantOperation; 11 | import io.eventuate.tram.sagas.simpledsl.annotations.SagaParticipantProxy; 12 | 13 | @SagaParticipantProxy(channel=CustomerServiceProxy.CHANNEL) 14 | public class CustomerServiceProxy { 15 | 16 | public static final String CHANNEL = "customerService"; 17 | 18 | public static final Class creditLimitExceededReply = CustomerCreditLimitExceeded.class; 19 | public static final Class customerNotFoundReply = CustomerNotFound.class; 20 | 21 | @SagaParticipantOperation(commandClass=ReserveCreditCommand.class, replyClasses=ReserveCreditResult.class) 22 | public CommandWithDestination reserveCredit(long orderId, Long customerId, Money orderTotal) { 23 | return CommandWithDestinationBuilder.send(new ReserveCreditCommand(customerId, orderId, orderTotal)) 24 | .to(CHANNEL) 25 | .build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /order-service/order-service-proxies-customer-service/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/proxies/customers/CustomerServiceProxyConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | public class CustomerServiceProxyConfiguration { 8 | 9 | @Bean 10 | public CustomerServiceProxy customerServiceProxy() { 11 | return new CustomerServiceProxy(); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /order-service/order-service-restapi/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | implementation project(":order-service:order-service-domain") 4 | implementation project(":order-service:order-service-sagas") 5 | implementation "io.eventuate.examples.common:eventuate-examples-common-money-jakarta9:$eventuateCommonExamplesVersion" 6 | implementation "org.springframework.boot:spring-boot-starter-data-jpa" 7 | 8 | implementation "org.springframework.boot:spring-boot-starter-web" 9 | testImplementation "org.mockito:mockito-core" 10 | testImplementation "io.rest-assured:spring-mock-mvc" 11 | 12 | } 13 | -------------------------------------------------------------------------------- /order-service/order-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/restapi/CreateOrderRequest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.restapi; 2 | 3 | 4 | import io.eventuate.examples.common.money.Money; 5 | 6 | public record CreateOrderRequest(Long customerId, Money orderTotal) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /order-service/order-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/restapi/CreateOrderResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.restapi; 2 | 3 | 4 | public record CreateOrderResponse(long orderId) { 5 | 6 | public Long getOrderId() { 7 | return orderId(); 8 | } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /order-service/order-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/restapi/GetOrderResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.restapi; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderState; 4 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.RejectionReason; 5 | 6 | public record GetOrderResponse(Long orderId, OrderState orderState, RejectionReason rejectionReason) { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /order-service/order-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/restapi/GetOrdersResponse.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.restapi; 2 | 3 | import java.util.List; 4 | 5 | public record GetOrdersResponse(List orders) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /order-service/order-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/restapi/OrderController.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.restapi; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.Order; 4 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderDetails; 5 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderRepository; 6 | import io.eventuate.examples.tram.sagas.customersandorders.orders.sagas.OrderSagaService; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.util.List; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.StreamSupport; 14 | 15 | @RestController 16 | public class OrderController { 17 | 18 | private final OrderSagaService orderSagaService; 19 | private final OrderRepository orderRepository; 20 | 21 | public OrderController(OrderSagaService orderSagaService, OrderRepository orderRepository) { 22 | this.orderSagaService = orderSagaService; 23 | this.orderRepository = orderRepository; 24 | } 25 | 26 | @PostMapping("/orders") 27 | public CreateOrderResponse createOrder(@RequestBody CreateOrderRequest createOrderRequest) { 28 | Order order = orderSagaService.createOrder(new OrderDetails(createOrderRequest.customerId(), createOrderRequest.orderTotal())); 29 | return new CreateOrderResponse(order.getId()); 30 | } 31 | 32 | @GetMapping("/orders") 33 | public ResponseEntity getAll() { 34 | return ResponseEntity.ok(new GetOrdersResponse(StreamSupport.stream(orderRepository.findAll().spliterator(), false) 35 | .map(o -> new GetOrderResponse(o.getId(), o.getState(), o.getRejectionReason())).collect(Collectors.toList()))); 36 | } 37 | 38 | @GetMapping("/orders/{orderId}") 39 | public ResponseEntity getOrder(@PathVariable Long orderId) { 40 | return orderRepository 41 | .findById(orderId) 42 | .map(o -> new ResponseEntity<>(new GetOrderResponse(o.getId(), o.getState(), o.getRejectionReason()), HttpStatus.OK)) 43 | .orElse(new ResponseEntity<>(HttpStatus.NOT_FOUND)); 44 | } 45 | 46 | @GetMapping("/orders/customer/{customerId}") 47 | public ResponseEntity> getOrdersByCustomerId(@PathVariable Long customerId) { 48 | return new ResponseEntity<>(orderRepository 49 | .findAllByOrderDetailsCustomerId(customerId) 50 | .stream() 51 | .map(o -> new GetOrderResponse(o.getId(), o.getState(), o.getRejectionReason())) 52 | .collect(Collectors.toList()), HttpStatus.OK); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /order-service/order-service-restapi/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/restapi/OrderRestApiConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.restapi; 2 | 3 | import org.springframework.context.annotation.ComponentScan; 4 | import org.springframework.context.annotation.Configuration; 5 | 6 | @Configuration 7 | @ComponentScan 8 | public class OrderRestApiConfiguration { 9 | 10 | 11 | } 12 | -------------------------------------------------------------------------------- /order-service/order-service-restapi/src/test/java/io/eventuate/examples/tram/sagas/ordersandorders/orders/web/OrderControllerTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.ordersandorders.orders.web; 2 | 3 | 4 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderRepository; 5 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderService; 6 | import io.eventuate.examples.tram.sagas.customersandorders.orders.restapi.OrderController; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.junit.jupiter.MockitoExtension; 12 | import org.springframework.http.HttpStatus; 13 | 14 | import java.util.Collections; 15 | 16 | import static io.restassured.http.ContentType.JSON; 17 | import static io.restassured.module.mockmvc.RestAssuredMockMvc.given; 18 | import static org.hamcrest.Matchers.empty; 19 | import static org.mockito.Mockito.when; 20 | 21 | @ExtendWith(MockitoExtension.class) 22 | public class OrderControllerTest { 23 | 24 | @Mock 25 | private OrderService orderService; 26 | @Mock 27 | private OrderRepository orderRepository; 28 | 29 | @InjectMocks 30 | private OrderController orderController; 31 | 32 | @Test 33 | public void shouldGetOrders() { 34 | when(orderRepository.findAll()).thenReturn(Collections.emptyList()); 35 | 36 | given() 37 | .standaloneSetup(orderController) 38 | .when() 39 | .get("/orders") 40 | .then() 41 | .log().ifValidationFails() 42 | .statusCode(HttpStatus.OK.value()) 43 | .contentType(JSON) 44 | .and().body("orders", empty()); 45 | } 46 | } -------------------------------------------------------------------------------- /order-service/order-service-sagas/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: IntegrationTestsPlugin 2 | 3 | repositories { 4 | maven { url "${project.rootDir}/build/repo" } 5 | } 6 | 7 | dependencies { 8 | implementation project(":order-service:order-service-domain") 9 | implementation "io.eventuate.examples.common:eventuate-examples-common-money-jakarta9:$eventuateCommonExamplesVersion" 10 | implementation project(":order-service:order-service-proxies-customer-service") 11 | 12 | implementation "io.eventuate.tram.core:eventuate-tram-spring-optimistic-locking" 13 | 14 | implementation "io.eventuate.tram.core:eventuate-tram-spring-jdbc-kafka" 15 | implementation "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-orchestration-simple-dsl-starter" 16 | 17 | implementation "io.eventuate.tram.core:eventuate-tram-spring-flyway" 18 | runtimeOnly "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-flyway" 19 | runtimeOnly "org.flywaydb:flyway-database-postgresql" 20 | implementation "org.flywaydb:flyway-core" 21 | implementation "org.flywaydb:flyway-mysql" 22 | 23 | 24 | testImplementation "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-testing-support" 25 | testImplementation "org.mockito:mockito-core" 26 | 27 | testImplementation "io.eventuate.tram.sagas:eventuate-tram-sagas-spring-in-memory" 28 | 29 | } 30 | 31 | -------------------------------------------------------------------------------- /order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/sagas/CreateOrderSaga.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.sagas; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditLimitExceeded; 5 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerNotFound; 6 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.Order; 7 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderService; 8 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.RejectionReason; 9 | import io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers.CustomerServiceProxy; 10 | import io.eventuate.tram.commands.consumer.CommandWithDestination; 11 | import io.eventuate.tram.sagas.orchestration.SagaDefinition; 12 | import io.eventuate.tram.sagas.simpledsl.SimpleSaga; 13 | 14 | import java.util.List; 15 | 16 | public class CreateOrderSaga implements SimpleSaga { 17 | 18 | private final OrderService orderService; 19 | private final CustomerServiceProxy customerService; 20 | 21 | public CreateOrderSaga(OrderService orderService, CustomerServiceProxy customerService) { 22 | this.orderService = orderService; 23 | this.customerService = customerService; 24 | } 25 | 26 | @Override 27 | public List getParticipantProxies() { 28 | return List.of(customerService); 29 | } 30 | 31 | private SagaDefinition sagaDefinition = 32 | step() 33 | .invokeLocal(this::create) 34 | .withCompensation(this::reject) 35 | .step() 36 | .invokeParticipant(this::reserveCredit) 37 | .onReply(CustomerServiceProxy.customerNotFoundReply, this::handleCustomerNotFound) 38 | .onReply(CustomerServiceProxy.creditLimitExceededReply, this::handleCustomerCreditLimitExceeded) 39 | .step() 40 | .invokeLocal(this::approve) 41 | .build(); 42 | 43 | private void handleCustomerNotFound(CreateOrderSagaData data, CustomerNotFound reply) { 44 | data.setRejectionReason(RejectionReason.UNKNOWN_CUSTOMER); 45 | } 46 | 47 | private void handleCustomerCreditLimitExceeded(CreateOrderSagaData data, CustomerCreditLimitExceeded reply) { 48 | data.setRejectionReason(RejectionReason.INSUFFICIENT_CREDIT); 49 | } 50 | 51 | 52 | @Override 53 | public SagaDefinition getSagaDefinition() { 54 | return this.sagaDefinition; 55 | } 56 | 57 | private void create(CreateOrderSagaData data) { 58 | Order order = orderService.createOrder(data.getOrderDetails()); 59 | data.setOrderId(order.getId()); 60 | } 61 | 62 | private CommandWithDestination reserveCredit(CreateOrderSagaData data) { 63 | long orderId = data.getOrderId(); 64 | Long customerId = data.getOrderDetails().customerId(); 65 | Money orderTotal = data.getOrderDetails().orderTotal(); 66 | return customerService.reserveCredit(orderId, customerId, orderTotal); 67 | } 68 | 69 | private void approve(CreateOrderSagaData data) { 70 | orderService.approveOrder(data.getOrderId()); 71 | } 72 | 73 | private void reject(CreateOrderSagaData data) { 74 | orderService.rejectOrder(data.getOrderId(), data.getRejectionReason()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/sagas/CreateOrderSagaData.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.sagas; 2 | 3 | 4 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderDetails; 5 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.RejectionReason; 6 | 7 | public class CreateOrderSagaData { 8 | 9 | private OrderDetails orderDetails; 10 | private Long orderId; 11 | private RejectionReason rejectionReason; 12 | 13 | public CreateOrderSagaData(OrderDetails orderDetails) { 14 | this.orderDetails = orderDetails; 15 | } 16 | 17 | public CreateOrderSagaData() { 18 | } 19 | 20 | public Long getOrderId() { 21 | return orderId; 22 | } 23 | 24 | public OrderDetails getOrderDetails() { 25 | return orderDetails; 26 | } 27 | 28 | public void setOrderId(Long orderId) { 29 | this.orderId = orderId; 30 | } 31 | 32 | public void setRejectionReason(RejectionReason rejectionReason) { 33 | this.rejectionReason = rejectionReason; 34 | } 35 | 36 | public RejectionReason getRejectionReason() { 37 | return rejectionReason; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/sagas/OrderSagaService.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.sagas; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.Order; 4 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderDetails; 5 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderRepository; 6 | import io.eventuate.tram.sagas.orchestration.SagaInstanceFactory; 7 | import org.springframework.transaction.annotation.Transactional; 8 | 9 | public class OrderSagaService { 10 | 11 | private final OrderRepository orderRepository; 12 | 13 | private final SagaInstanceFactory sagaInstanceFactory; 14 | 15 | private final CreateOrderSaga createOrderSaga; 16 | 17 | public OrderSagaService(OrderRepository orderRepository, SagaInstanceFactory sagaInstanceFactory, CreateOrderSaga createOrderSaga) { 18 | this.orderRepository = orderRepository; 19 | this.sagaInstanceFactory = sagaInstanceFactory; 20 | this.createOrderSaga = createOrderSaga; 21 | } 22 | 23 | @Transactional 24 | public Order createOrder(OrderDetails orderDetails) { 25 | CreateOrderSagaData data = new CreateOrderSagaData(orderDetails); 26 | sagaInstanceFactory.create(createOrderSaga, data); 27 | return orderRepository.findById(data.getOrderId()).get(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /order-service/order-service-sagas/src/main/java/io/eventuate/examples/tram/sagas/customersandorders/orders/sagas/OrderSagasConfiguration.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.sagas; 2 | 3 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderRepository; 4 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.OrderService; 5 | import io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers.CustomerServiceProxy; 6 | import io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers.CustomerServiceProxyConfiguration; 7 | import io.eventuate.tram.sagas.orchestration.SagaInstanceFactory; 8 | import io.eventuate.tram.spring.flyway.EventuateTramFlywayMigrationConfiguration; 9 | import io.eventuate.tram.spring.optimisticlocking.OptimisticLockingDecoratorConfiguration; 10 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 11 | import org.springframework.context.annotation.Bean; 12 | import org.springframework.context.annotation.Configuration; 13 | import org.springframework.context.annotation.Import; 14 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 15 | 16 | @Configuration 17 | @EnableJpaRepositories 18 | @EnableAutoConfiguration 19 | @Import({OptimisticLockingDecoratorConfiguration.class, CustomerServiceProxyConfiguration.class, EventuateTramFlywayMigrationConfiguration.class}) 20 | public class OrderSagasConfiguration { 21 | 22 | @Bean 23 | public OrderSagaService orderSagaService(OrderRepository orderRepository, SagaInstanceFactory sagaInstanceFactory, CreateOrderSaga createOrderSaga) { 24 | return new OrderSagaService(orderRepository, sagaInstanceFactory, createOrderSaga); 25 | } 26 | 27 | @Bean 28 | public CreateOrderSaga createOrderSaga(OrderService orderService, CustomerServiceProxy customerService) { 29 | return new CreateOrderSaga(orderService, customerService); 30 | } 31 | 32 | 33 | } 34 | -------------------------------------------------------------------------------- /order-service/order-service-sagas/src/test/java/io/eventuate/examples/tram/sagas/customersandorders/orders/service/CreateOrderSagaTest.java: -------------------------------------------------------------------------------- 1 | package io.eventuate.examples.tram.sagas.customersandorders.orders.service; 2 | 3 | import io.eventuate.examples.common.money.Money; 4 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.commands.ReserveCreditCommand; 5 | import io.eventuate.examples.tram.sagas.customersandorders.customers.creditreservationapi.replies.CustomerCreditLimitExceeded; 6 | import io.eventuate.examples.tram.sagas.customersandorders.orders.domain.*; 7 | import io.eventuate.examples.tram.sagas.customersandorders.orders.proxies.customers.CustomerServiceProxy; 8 | import io.eventuate.examples.tram.sagas.customersandorders.orders.sagas.CreateOrderSaga; 9 | import io.eventuate.examples.tram.sagas.customersandorders.orders.sagas.CreateOrderSagaData; 10 | import org.junit.jupiter.api.BeforeEach; 11 | import org.junit.jupiter.api.Test; 12 | import org.mockito.stubbing.Answer; 13 | 14 | import java.util.Optional; 15 | 16 | import static io.eventuate.tram.sagas.testing.SagaUnitTestSupport.given; 17 | import static org.junit.jupiter.api.Assertions.assertEquals; 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.Mockito.mock; 20 | import static org.mockito.Mockito.when; 21 | 22 | public class CreateOrderSagaTest { 23 | 24 | private OrderRepository orderRepository; 25 | private OrderService orderService; 26 | private Long customerId = 102L; 27 | private Money orderTotal = new Money("12.34"); 28 | private OrderDetails orderDetails = new OrderDetails(customerId, orderTotal); 29 | private Long orderId = 103L; 30 | private CustomerServiceProxy customerService = new CustomerServiceProxy(); 31 | 32 | private CreateOrderSaga makeCreateOrderSaga() { 33 | return new CreateOrderSaga(orderService, customerService); 34 | } 35 | 36 | 37 | @BeforeEach 38 | public void setUp() { 39 | orderRepository = mock(OrderRepository.class); 40 | orderService = new OrderService(orderRepository); 41 | } 42 | 43 | private Order order; 44 | 45 | @Test 46 | public void shouldCreateOrder() { 47 | when(orderRepository.save(any(Order.class))).then((Answer) invocation -> { 48 | order = invocation.getArgument(0); 49 | order.setId(orderId); 50 | return order; 51 | }); 52 | 53 | when(orderRepository.findById(orderId)).then(invocation -> Optional.of(order)); 54 | 55 | given() 56 | .saga(makeCreateOrderSaga(), 57 | new CreateOrderSagaData(orderDetails)). 58 | expect(). 59 | command(new ReserveCreditCommand(customerId, orderId, orderTotal)) 60 | .to("customerService") 61 | .andGiven() 62 | .successReply() 63 | .expectCompletedSuccessfully(); 64 | 65 | assertEquals(OrderState.APPROVED, order.getState()); 66 | } 67 | 68 | @Test 69 | public void shouldRejectCreateOrder() { 70 | when(orderRepository.save(any(Order.class))).then((Answer) invocation -> { 71 | order = invocation.getArgument(0); 72 | order.setId(orderId); 73 | return order; 74 | }); 75 | 76 | when(orderRepository.findById(orderId)).then(invocation -> Optional.of(order)); 77 | 78 | CreateOrderSagaData data = new CreateOrderSagaData(orderDetails); 79 | 80 | given() 81 | .saga(makeCreateOrderSaga(), 82 | data). 83 | expect(). 84 | command(new ReserveCreditCommand(customerId, orderId, orderTotal)) 85 | .to("customerService") 86 | .andGiven() 87 | .failureReply(new CustomerCreditLimitExceeded()) 88 | .expectRolledBack() 89 | .assertSagaData(sd -> 90 | assertEquals(RejectionReason.INSUFFICIENT_CREDIT, sd.getRejectionReason())); 91 | 92 | assertEquals(OrderState.REJECTED, order.getState()); 93 | assertEquals(RejectionReason.INSUFFICIENT_CREDIT, order.getRejectionReason()); 94 | } 95 | } -------------------------------------------------------------------------------- /postgres-customer-service-cli.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash -e 2 | 3 | IMAGE=$(docker ps --filter "network=CustomersAndOrdersEndToEndTest" --format "{{.Image}}" \ 4 | | grep postgres | head -1) 5 | 6 | docker run ${1:--it} \ 7 | --network=CustomersAndOrdersEndToEndTest \ 8 | --rm "$IMAGE" \ 9 | sh -c 'export PGPASSWORD=postgrespw; exec psql -h "customer-service-db" -U postgresuser eventuate' 10 | -------------------------------------------------------------------------------- /postgres-order-service-cli.sh: -------------------------------------------------------------------------------- 1 | π#! /bin/bash -e 2 | 3 | IMAGE=$(docker ps --filter "network=CustomersAndOrdersEndToEndTest" --format "{{.Image}}" \ 4 | | grep postgres | head -1) 5 | 6 | docker run ${1:--it} \ 7 | --network=CustomersAndOrdersEndToEndTest \ 8 | --rm "$IMAGE" \ 9 | sh -c 'export PGPASSWORD=postgrespw; exec psql -h "order-service-db" -U postgresuser eventuate' 10 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "eventuate-tram-sagas-examples-customers-and-orders" 2 | 3 | include 'customer-service:customer-service-domain' 4 | include 'customer-service:customer-service-restapi' 5 | include 'customer-service:customer-service-main' 6 | include 'customer-service:customer-service-credit-reservation-api' 7 | include 'customer-service:customer-service-persistence' 8 | 9 | include 'order-service:order-service-domain' 10 | include 'order-service:order-service-proxies-customer-service' 11 | include 'order-service:order-service-sagas' 12 | include 'order-service:order-service-restapi' 13 | include 'order-service:order-service-persistence' 14 | include 'order-service:order-service-main' 15 | 16 | include 'api-gateway-service:api-gateway-service-main' 17 | 18 | include 'end-to-end-tests' 19 | -------------------------------------------------------------------------------- /test-curl-commands.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | set -o pipefail 4 | 5 | API_GATEWAY_SERVICE_CONTAINER=$(docker ps --filter "network=CustomersAndOrdersEndToEndTest" \ 6 | --filter "label=io.eventuate.name=api-gateway-service" -q) 7 | echo "API_GATEWAY_SERVICE_CONTAINER=$API_GATEWAY_SERVICE_CONTAINER" 8 | 9 | API_GATEWAY_SERVICE_PORT=$(docker port "$API_GATEWAY_SERVICE_CONTAINER" 8080 | cut -d':' -f2) 10 | 11 | FILE_PATH="end-to-end-tests/src/endToEndTest/resources/templates/index.html" 12 | 13 | awk ' 14 | /
/ {flag=1; next}
15 |   /<\/code><\/pre>/ {flag=0; if (full_cmd != "") {print full_cmd; full_cmd=""}}
16 |   flag {full_cmd = (full_cmd ? full_cmd " " : "") $0}
17 | ' "$FILE_PATH" | sed -e "s/\[\[\${apiGatewayUrl}\]\]/localhost:$API_GATEWAY_SERVICE_PORT/" | while read -r cmd; do
18 |     echo "Executing: $cmd"
19 |     eval "$cmd"
20 | done
21 | 
22 | 


--------------------------------------------------------------------------------
/wait-for-mysql.sh:
--------------------------------------------------------------------------------
1 | #! /bin/sh
2 | 
3 | until (echo select 1 from dual | ./mysql-cli.sh -i > /dev/null)
4 | do
5 |  echo sleeping for mysql
6 |  sleep 5
7 | done
8 | 


--------------------------------------------------------------------------------
/wait-for-postgres.sh:
--------------------------------------------------------------------------------
1 | #! /bin/sh
2 | 
3 | until (echo select 1 | ./postgres-cli.sh -i > /dev/null)
4 | do
5 |  echo sleeping for postgres
6 |  sleep 5
7 | done
8 | 


--------------------------------------------------------------------------------
/wait-for-services.sh:
--------------------------------------------------------------------------------
 1 | #! /bin/bash
 2 | 
 3 | done=false
 4 | 
 5 | echo waiting for: $*
 6 | 
 7 | host=${1?}
 8 | shift
 9 | health_url=${1?}
10 | shift
11 | ports=$*
12 | 
13 | if [ -z "$ports" ] ; then
14 | 	echo no ports
15 | 	exit 99
16 | fi
17 | 
18 | while [[ "$done" = false ]]; do
19 | 	for port in $ports; do
20 | 		curl --fail http://${host}:${port}/${health_url} >& /dev/null
21 | 		if [[ "$?" -eq "0" ]]; then
22 | 			done=true
23 | 		else
24 | 			done=false
25 | 			break
26 | 		fi
27 | 	done
28 | 	if [[ "$done" = true ]]; then
29 | 		echo connected
30 | 		break;
31 |   fi
32 | 	echo -n .
33 | 	sleep 1
34 | done
35 | 


--------------------------------------------------------------------------------