├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── README.md ├── axon-event-commons ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── ivanfranchin │ └── axoneventcommons │ ├── customer │ ├── CustomerAddedEvent.java │ ├── CustomerDeletedEvent.java │ ├── CustomerEvent.java │ └── CustomerUpdatedEvent.java │ ├── order │ ├── OrderCreatedEvent.java │ └── OrderEvent.java │ ├── restaurant │ ├── RestaurantAddedEvent.java │ ├── RestaurantDeletedEvent.java │ ├── RestaurantDishAddedEvent.java │ ├── RestaurantDishDeletedEvent.java │ ├── RestaurantDishUpdatedEvent.java │ ├── RestaurantEvent.java │ └── RestaurantUpdatedEvent.java │ └── util │ └── MyStringUtils.java ├── build-docker-images.sh ├── customer-service ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── customerservice │ │ │ ├── CustomerServiceApplication.java │ │ │ ├── aggregate │ │ │ └── CustomerAggregate.java │ │ │ ├── command │ │ │ ├── AddCustomerCommand.java │ │ │ ├── DeleteCustomerCommand.java │ │ │ └── UpdateCustomerCommand.java │ │ │ ├── config │ │ │ ├── AxonConfig.java │ │ │ ├── ErrorAttributesConfig.java │ │ │ └── SwaggerConfig.java │ │ │ ├── exception │ │ │ └── CustomerNotFoundException.java │ │ │ ├── interceptor │ │ │ ├── CommandLoggingDispatchInterceptor.java │ │ │ └── EventLoggingDispatchInterceptor.java │ │ │ ├── model │ │ │ ├── Customer.java │ │ │ ├── Order.java │ │ │ └── OrderItem.java │ │ │ ├── query │ │ │ ├── GetCustomerOrdersQuery.java │ │ │ ├── GetCustomerQuery.java │ │ │ └── GetCustomersQuery.java │ │ │ ├── repository │ │ │ ├── CustomerRepository.java │ │ │ ├── CustomerRepositoryProjector.java │ │ │ └── OrderRepository.java │ │ │ ├── rest │ │ │ ├── CustomerController.java │ │ │ ├── IndexController.java │ │ │ └── dto │ │ │ │ ├── AddCustomerRequest.java │ │ │ │ ├── CustomerOrderResponse.java │ │ │ │ ├── CustomerResponse.java │ │ │ │ └── UpdateCustomerRequest.java │ │ │ └── websocket │ │ │ ├── WebSocketConfig.java │ │ │ └── WebSocketHandler.java │ └── resources │ │ ├── application.properties │ │ ├── banner.txt │ │ ├── static │ │ └── app.js │ │ └── templates │ │ └── index.html │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── customerservice │ └── CustomerServiceApplicationTests.java ├── docker-compose.yml ├── documentation ├── axon-server.jpeg ├── customer-service.jpeg ├── demo.gif ├── food-ordering-service.jpeg ├── project-diagram.excalidraw ├── project-diagram.jpeg └── restaurant-service.jpeg ├── food-ordering-service ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── foodorderingservice │ │ │ ├── FoodOrderingServiceApplication.java │ │ │ ├── config │ │ │ ├── AxonConfig.java │ │ │ ├── ErrorAttributesConfig.java │ │ │ ├── MongoConfig.java │ │ │ └── SwaggerConfig.java │ │ │ ├── customer │ │ │ ├── exception │ │ │ │ └── CustomerNotFoundException.java │ │ │ ├── model │ │ │ │ └── Customer.java │ │ │ ├── repository │ │ │ │ ├── CustomerRepository.java │ │ │ │ └── CustomerRepositoryProjector.java │ │ │ ├── rest │ │ │ │ ├── CustomerController.java │ │ │ │ └── dto │ │ │ │ │ └── CustomerResponse.java │ │ │ └── service │ │ │ │ ├── CustomerService.java │ │ │ │ └── CustomerServiceImpl.java │ │ │ ├── order │ │ │ ├── aggregate │ │ │ │ └── OrderAggregate.java │ │ │ ├── command │ │ │ │ └── CreateOrderCommand.java │ │ │ ├── exception │ │ │ │ └── OrderNotFoundException.java │ │ │ ├── interceptor │ │ │ │ ├── CommandLoggingDispatchInterceptor.java │ │ │ │ └── EventLoggingDispatchInterceptor.java │ │ │ ├── model │ │ │ │ ├── Order.java │ │ │ │ ├── OrderItem.java │ │ │ │ └── OrderStatus.java │ │ │ ├── query │ │ │ │ ├── GetOrderQuery.java │ │ │ │ └── GetOrdersQuery.java │ │ │ ├── repository │ │ │ │ ├── OrderRepository.java │ │ │ │ └── OrderRepositoryProjector.java │ │ │ ├── rest │ │ │ │ ├── IndexController.java │ │ │ │ ├── OrderController.java │ │ │ │ └── dto │ │ │ │ │ ├── CreateOrderRequest.java │ │ │ │ │ └── OrderResponse.java │ │ │ └── websocket │ │ │ │ ├── WebSocketConfig.java │ │ │ │ └── WebSocketHandler.java │ │ │ └── restaurant │ │ │ ├── exception │ │ │ ├── DishNotFoundException.java │ │ │ └── RestaurantNotFoundException.java │ │ │ ├── model │ │ │ ├── Dish.java │ │ │ └── Restaurant.java │ │ │ ├── repository │ │ │ ├── RestaurantRepository.java │ │ │ └── RestaurantRepositoryProjector.java │ │ │ ├── rest │ │ │ ├── RestaurantController.java │ │ │ └── dto │ │ │ │ └── RestaurantResponse.java │ │ │ └── service │ │ │ ├── RestaurantService.java │ │ │ └── RestaurantServiceImpl.java │ └── resources │ │ ├── application.properties │ │ ├── banner.txt │ │ ├── static │ │ └── app.js │ │ └── templates │ │ └── index.html │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── foodorderingservice │ └── FoodOrderingServiceApplicationTests.java ├── mvnw ├── mvnw.cmd ├── pom.xml ├── remove-docker-images.sh ├── restaurant-service ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── ivanfranchin │ │ │ └── restaurantservice │ │ │ ├── RestaurantServiceApplication.java │ │ │ ├── aggregate │ │ │ └── RestaurantAggregate.java │ │ │ ├── command │ │ │ ├── AddRestaurantCommand.java │ │ │ ├── AddRestaurantDishCommand.java │ │ │ ├── DeleteRestaurantCommand.java │ │ │ ├── DeleteRestaurantDishCommand.java │ │ │ ├── UpdateRestaurantCommand.java │ │ │ └── UpdateRestaurantDishCommand.java │ │ │ ├── config │ │ │ ├── AxonConfig.java │ │ │ ├── ErrorAttributesConfig.java │ │ │ └── SwaggerConfig.java │ │ │ ├── exception │ │ │ ├── DishNotFoundException.java │ │ │ └── RestaurantNotFoundException.java │ │ │ ├── interceptor │ │ │ ├── CommandLoggingDispatchInterceptor.java │ │ │ └── EventLoggingDispatchInterceptor.java │ │ │ ├── model │ │ │ ├── Dish.java │ │ │ ├── Order.java │ │ │ ├── OrderItem.java │ │ │ └── Restaurant.java │ │ │ ├── query │ │ │ ├── GetRestaurantDishQuery.java │ │ │ ├── GetRestaurantOrdersQuery.java │ │ │ ├── GetRestaurantQuery.java │ │ │ └── GetRestaurantsQuery.java │ │ │ ├── repository │ │ │ ├── OrderRepository.java │ │ │ ├── RestaurantRepository.java │ │ │ └── RestaurantRepositoryProjector.java │ │ │ ├── rest │ │ │ ├── IndexController.java │ │ │ ├── RestaurantController.java │ │ │ └── dto │ │ │ │ ├── AddRestaurantDishRequest.java │ │ │ │ ├── AddRestaurantRequest.java │ │ │ │ ├── DishResponse.java │ │ │ │ ├── RestaurantOrderResponse.java │ │ │ │ ├── RestaurantResponse.java │ │ │ │ ├── UpdateRestaurantDishRequest.java │ │ │ │ └── UpdateRestaurantRequest.java │ │ │ └── websocket │ │ │ ├── WebSocketConfig.java │ │ │ └── WebSocketHandler.java │ └── resources │ │ ├── application.properties │ │ ├── banner.txt │ │ ├── static │ │ └── app.js │ │ └── templates │ │ └── index.html │ └── test │ └── java │ └── com │ └── ivanfranchin │ └── restaurantservice │ └── RestaurantServiceApplicationTests.java ├── scripts └── my-functions.sh ├── start-apps.sh └── stop-apps.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | /mvnw text eol=lf 2 | *.cmd text eol=crlf 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ivangfr 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | .sts4-cache 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | build/ 28 | !**/src/main/**/build/ 29 | !**/src/test/**/build/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | 34 | ### MAC OS ### 35 | *.DS_Store 36 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionType=only-script 19 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axon-springboot-websocket 2 | 3 | The goal of this project is to play with [`Axon`](https://axoniq.io/). For this, we will implement a `food-ordering` app that consists of three [`Spring Boot`](https://docs.spring.io/spring-boot/index.html) applications: `customer-service`, `restaurant-service`, and `food-ordering-service`. These services were implemented with [`CQRS`](https://martinfowler.com/bliki/CQRS.html) and [`Event Sourcing`](https://martinfowler.com/eaaDev/EventSourcing.html) in mind. To achieve this, we used the [`Axon Framework`](https://www.axoniq.io/products/axon-framework). The three services are connected to `axon-server`, which is the [`Event Store`](https://en.wikipedia.org/wiki/Event_store) and `Message Routing` solution used. 4 | 5 | ## Proof-of-Concepts & Articles 6 | 7 | On [ivangfr.github.io](https://ivangfr.github.io), I have compiled my Proof-of-Concepts (PoCs) and articles. You can easily search for the technology you are interested in by using the filter. Who knows, perhaps I have already implemented a PoC or written an article about what you are looking for. 8 | 9 | ## Project Diagram 10 | 11 | ![project-diagram](documentation/project-diagram.jpeg) 12 | 13 | ## Applications 14 | 15 | - ### customer-service 16 | 17 | `Spring Boot` application that exposes a REST API to manage `Customers`. It also has a UI implemented using `JavaScript`, `jQuery`, and `Semantic UI`. 18 | 19 | ![customer-service](documentation/customer-service.jpeg) 20 | 21 | `customer-service` was implemented using the `Axon Framework`. Every time a customer is added, updated, or deleted, the service emits the respective event, i.e., `CustomerAddedEvent`, `CustomerUpdatedEvent`, or `CustomerDeletedEvent`. 22 | 23 | `customer-service` uses `MySQL` to store customer data. Additionally, it listens to order events, collects the order information that it needs, and stores them in an order table present in its own database, so that it doesn't need to call another service to get this information. 24 | 25 | - ### restaurant-service 26 | 27 | `Spring Boot` application that exposes a REST API to manage `Restaurants`. It also has a UI implemented using `JavaScript`, `jQuery`, and `Semantic UI`. 28 | 29 | ![restaurant-service](documentation/restaurant-service.jpeg) 30 | 31 | `restaurant-service` was implemented using the `Axon Framework`. Every time a restaurant is added, updated, or deleted, the service emits the respective event, i.e., `RestaurantAddedEvent`, `RestaurantUpdatedEvent`, or `RestaurantDeletedEvent`. The same applies to the restaurant dishes, whose events are: `RestaurantDishAddedEvent`, `RestaurantDishUpdatedEvent`, or `RestaurantDishDeletedEvent`. 32 | 33 | `restaurant-service` uses `PostgreSQL` to store restaurant/dish data. Additionally, it listens to order events, collects the order information that it needs, and stores them in an order table present in its own database, so that it doesn't need to call another service to get this information. 34 | 35 | - ### food-ordering-service 36 | 37 | `Spring Boot` application that exposes a REST API to manage `Orders`. It has a UI implemented using `JavaScript`, `jQuery`, and `Semantic UI`. 38 | 39 | ![food-ordering-service](documentation/food-ordering-service.jpeg) 40 | 41 | `food-ordering-service` was implemented using the `Axon Framework`. Every time an order is created, the service emits the respective event, i.e., `OrderCreatedEvent`. 42 | 43 | `food-ordering-service` uses `MongoDB` to store order data. Additionally, it listens to customer and restaurant/dish events, collects the information that it needs, and stores them in a customer or restaurant/dish table present in its own database, so that it doesn't need to call another service to get this information. 44 | 45 | - ### axon-event-commons 46 | 47 | `Maven` project where all events mentioned above are defined. It generates a JAR file that is added as a dependency in the `pom.xml` of `customer-service`, `restaurant-service`, and `food-ordering-service`. 48 | 49 | ## Prerequisites 50 | 51 | - [`Java 17`](https://www.oracle.com/java/technologies/downloads/#java17) or higher; 52 | - A containerization tool (e.g., [`Docker`](https://www.docker.com), [`Podman`](https://podman.io), etc.) 53 | 54 | ## Start Environment 55 | 56 | - Open a terminal and inside the `axon-springboot-websocket` root folder run: 57 | ```bash 58 | docker compose up -d 59 | ``` 60 | 61 | - Wait for Docker containers to be up and running. To check it, run: 62 | ```bash 63 | docker ps -a 64 | ``` 65 | 66 | ## Running applications with Maven 67 | 68 | Inside the `axon-springboot-websocket` root folder, run the following commands in different terminals: 69 | 70 | - **axon-event-commons** 71 | ```bash 72 | ./mvnw clean install --projects axon-event-commons 73 | ``` 74 | 75 | - **customer-service** 76 | ```bash 77 | ./mvnw clean spring-boot:run --projects customer-service -Dspring-boot.run.jvmArguments="-Dserver.port=9080" 78 | ``` 79 | 80 | - **restaurant-service** 81 | ```bash 82 | ./mvnw clean spring-boot:run --projects restaurant-service -Dspring-boot.run.jvmArguments="-Dserver.port=9081" 83 | ``` 84 | 85 | - **food-ordering-service** 86 | ```bash 87 | ./mvnw clean spring-boot:run --projects food-ordering-service -Dspring-boot.run.jvmArguments="-Dserver.port=9082" 88 | ``` 89 | 90 | ## Running applications as Docker containers 91 | 92 | - ### Build Docker images 93 | 94 | - In a terminal, make sure you are in the `axon-springboot-websocket` root folder; 95 | - Run the following script to build the Docker images: 96 | ```bash 97 | ./build-docker-images.sh 98 | ``` 99 | 100 | - ### Environment Variables 101 | 102 | - **customer-service** 103 | 104 | | Environment Variable | Description | 105 | |----------------------|-----------------------------------------------------------------------| 106 | | `MYSQL_HOST` | Specify the host of the `MySQL` database to use (default `localhost`) | 107 | | `MYSQL_PORT` | Specify the port of the `MySQL` database to use (default `3306`) | 108 | | `AXON_SERVER_HOST` | Specify the host of the `Axon Server` to use (default `localhost`) | 109 | | `AXON_SERVER_PORT` | Specify the port of the `Axon Server` to use (default `8124`) | 110 | 111 | - **restaurant-service** 112 | 113 | | Environment Variable | Description | 114 | |----------------------|--------------------------------------------------------------------------| 115 | | `POSTGRES_HOST` | Specify the host of the `Postgres` database to use (default `localhost`) | 116 | | `POSTGRES_PORT` | Specify the port of the `Postgres` database to use (default `5432`) | 117 | | `AXON_SERVER_HOST` | Specify the host of the `Axon Server` to use (default `localhost`) | 118 | | `AXON_SERVER_PORT` | Specify the port of the `Axon Server` to use (default `8124`) | 119 | 120 | - **food-ordering-service** 121 | 122 | | Environment Variable | Description | 123 | |----------------------|-----------------------------------------------------------------------| 124 | | `MONGODB_HOST` | Specify the host of the `Mongo` database to use (default `localhost`) | 125 | | `MONGODB_PORT` | Specify the port of the `Mongo` database to use (default `27017`) | 126 | | `AXON_SERVER_HOST` | Specify the host of the `Axon Server` to use (default `localhost`) | 127 | | `AXON_SERVER_PORT` | Specify the port of the `Axon Server` to use (default `8124`) | 128 | 129 | - ### Start Docker containers 130 | 131 | - In a terminal, make sure you are inside the `axon-springboot-websocket` root folder; 132 | - Run the following command: 133 | ```bash 134 | ./start-apps.sh 135 | ``` 136 | 137 | ## Application URLs 138 | 139 | | Application | URL | Swagger | 140 | |-----------------------|-----------------------|---------------------------------------| 141 | | customer-service | http://localhost:9080 | http://localhost:9080/swagger-ui.html | 142 | | restaurant-service | http://localhost:9081 | http://localhost:9081/swagger-ui.html | 143 | | food-ordering-service | http://localhost:9082 | http://localhost:9082/swagger-ui.html | 144 | 145 | ## Demo 146 | 147 | The GIF below shows a user creating a customer in the `customer-service` UI. Then, in the `restaurant-service` UI, they create a restaurant and add a dish. Finally, using the `food-ordering-service` UI, they submit an order using the customer and restaurant/dish created. Note that as soon as a customer or restaurant/dish is created, an event is sent, and the consumer of this event updates its UI in real-time using WebSockets. 148 | 149 | ![demo](documentation/demo.gif) 150 | 151 | ## Useful Commands & Links 152 | 153 | - **Axon Server** 154 | 155 | The `Axon Server` dashboard can be accessed at http://localhost:8024 156 | 157 | ![axon-server](documentation/axon-server.jpeg) 158 | 159 | - **MySQL** 160 | ```bash 161 | docker exec -it -e MYSQL_PWD=secret mysql mysql -uroot --database customerdb 162 | 163 | SELECT * FROM customers; 164 | SELECT * FROM orders; 165 | ``` 166 | > Type `exit` to exit 167 | 168 | - **PostgreSQL** 169 | ```bash 170 | docker exec -it postgres psql -U postgres -d restaurantdb 171 | 172 | SELECT * FROM restaurants; 173 | SELECT * FROM dishes; 174 | SELECT * FROM orders; 175 | ``` 176 | > Type `\q` to exit 177 | 178 | - **MongoDB** 179 | ```bash 180 | docker exec -it mongodb mongo foodorderingdb 181 | 182 | db.customers.find() 183 | db.restaurants.find() 184 | db.orders.find() 185 | ``` 186 | > Type `exit` to exit 187 | 188 | ## Shutdown 189 | 190 | - To stop applications: 191 | - If you start them with `Maven`, go to the terminals where they are running and press `Ctrl+C`; 192 | - If you start them as Docker containers, make sure you are inside the `axon-springboot-websocket` root folder and run the following script: 193 | ```bash 194 | ./stop-apps.sh 195 | ``` 196 | - To stop and remove docker compose containers, network, and volumes, go to a terminal and, inside the `axon-springboot-websocket` root folder, run the command below: 197 | ```bash 198 | docker compose down -v 199 | ``` 200 | 201 | ## Cleanup 202 | 203 | To remove the docker images created by this project, go to a terminal and, inside the `axon-springboot-websocket` root folder, run the following script: 204 | ```bash 205 | ./remove-docker-images.sh 206 | ``` 207 | 208 | ## References 209 | 210 | - https://sgitario.github.io/axon-by-example/ 211 | - https://blog.nebrass.fr/playing-with-cqrs-and-event-sourcing-in-spring-boot-and-axon/ 212 | -------------------------------------------------------------------------------- /axon-event-commons/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | com.ivanfranchin 6 | axon-event-commons 7 | 1.0.0 8 | axon-event-commons 9 | Demo project for Spring Boot 10 | 11 | UTF-8 12 | 17 13 | 17 14 | 15 | 16 | 17 | org.projectlombok 18 | lombok 19 | 1.18.38 20 | true 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/customer/CustomerAddedEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.customer; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class CustomerAddedEvent implements CustomerEvent { 11 | 12 | private String id; 13 | private String name; 14 | private String address; 15 | } 16 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/customer/CustomerDeletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.customer; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class CustomerDeletedEvent implements CustomerEvent { 11 | 12 | private String id; 13 | } 14 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/customer/CustomerEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.customer; 2 | 3 | public interface CustomerEvent { 4 | } 5 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/customer/CustomerUpdatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.customer; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class CustomerUpdatedEvent implements CustomerEvent { 11 | 12 | private String id; 13 | private String name; 14 | private String address; 15 | } 16 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/order/OrderCreatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.order; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.math.BigDecimal; 8 | import java.time.ZonedDateTime; 9 | import java.util.Set; 10 | 11 | @Data 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class OrderCreatedEvent implements OrderEvent { 15 | 16 | private String id; 17 | private String customerId; 18 | private String customerName; 19 | private String customerAddress; 20 | private String restaurantId; 21 | private String restaurantName; 22 | private String status; 23 | private BigDecimal total; 24 | private ZonedDateTime createdAt; 25 | private Set items; 26 | 27 | @Data 28 | @AllArgsConstructor 29 | @NoArgsConstructor 30 | public static class OrderItem { 31 | private String dishId; 32 | private String dishName; 33 | private BigDecimal dishPrice; 34 | private Short quantity; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/order/OrderEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.order; 2 | 3 | public interface OrderEvent { 4 | } 5 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/restaurant/RestaurantAddedEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.restaurant; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class RestaurantAddedEvent implements RestaurantEvent { 11 | 12 | private String id; 13 | private String name; 14 | } 15 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/restaurant/RestaurantDeletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.restaurant; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class RestaurantDeletedEvent implements RestaurantEvent { 11 | 12 | private String id; 13 | } 14 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/restaurant/RestaurantDishAddedEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.restaurant; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.math.BigDecimal; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class RestaurantDishAddedEvent implements RestaurantEvent { 13 | 14 | private String restaurantId; 15 | private String dishId; 16 | private String dishName; 17 | private BigDecimal dishPrice; 18 | } 19 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/restaurant/RestaurantDishDeletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.restaurant; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class RestaurantDishDeletedEvent implements RestaurantEvent { 11 | 12 | private String restaurantId; 13 | private String dishId; 14 | } 15 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/restaurant/RestaurantDishUpdatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.restaurant; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.math.BigDecimal; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class RestaurantDishUpdatedEvent implements RestaurantEvent { 13 | 14 | private String restaurantId; 15 | private String dishId; 16 | private String dishName; 17 | private BigDecimal dishPrice; 18 | } 19 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/restaurant/RestaurantEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.restaurant; 2 | 3 | public interface RestaurantEvent { 4 | } 5 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/restaurant/RestaurantUpdatedEvent.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.restaurant; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class RestaurantUpdatedEvent implements RestaurantEvent { 11 | 12 | private String id; 13 | private String name; 14 | } 15 | -------------------------------------------------------------------------------- /axon-event-commons/src/main/java/com/ivanfranchin/axoneventcommons/util/MyStringUtils.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.axoneventcommons.util; 2 | 3 | public final class MyStringUtils { 4 | 5 | public static String getTrimmedValueOrElse(String value, String fallbackValue) { 6 | String newValue = value == null ? fallbackValue : value.trim(); 7 | return "".equals(newValue) ? fallbackValue : newValue; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /build-docker-images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DOCKER_IMAGE_PREFIX="ivanfranchin" 4 | APP_VERSION="1.0.0" 5 | 6 | CUSTOMER_SERVICE_APP_NAME="customer-service" 7 | RESTAURANT_SERVICE_APP_NAME="restaurant-service" 8 | FOOD_ORDERING_SERVICE_APP_NAME="food-ordering-service" 9 | 10 | CUSTOMER_SERVICE_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${CUSTOMER_SERVICE_APP_NAME}:${APP_VERSION}" 11 | RESTAURANT_SERVICE_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${RESTAURANT_SERVICE_APP_NAME}:${APP_VERSION}" 12 | FOOD_ORDERING_SERVICE_DOCKER_IMAGE_NAME="${DOCKER_IMAGE_PREFIX}/${FOOD_ORDERING_SERVICE_APP_NAME}:${APP_VERSION}" 13 | 14 | SKIP_TESTS="true" 15 | 16 | ./mvnw clean install --projects axon-event-commons 17 | 18 | ./mvnw clean spring-boot:build-image \ 19 | --projects "$CUSTOMER_SERVICE_APP_NAME" \ 20 | -DskipTests="$SKIP_TESTS" \ 21 | -Dspring-boot.build-image.imageName="$CUSTOMER_SERVICE_DOCKER_IMAGE_NAME" 22 | 23 | ./mvnw clean spring-boot:build-image \ 24 | --projects "$RESTAURANT_SERVICE_APP_NAME" \ 25 | -DskipTests="$SKIP_TESTS" \ 26 | -Dspring-boot.build-image.imageName="$RESTAURANT_SERVICE_DOCKER_IMAGE_NAME" 27 | 28 | ./mvnw clean spring-boot:build-image \ 29 | --projects "$FOOD_ORDERING_SERVICE_APP_NAME" \ 30 | -DskipTests="$SKIP_TESTS" \ 31 | -Dspring-boot.build-image.imageName="$FOOD_ORDERING_SERVICE_DOCKER_IMAGE_NAME" 32 | -------------------------------------------------------------------------------- /customer-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | axon-springboot-websocket 8 | 1.0.0 9 | ../pom.xml 10 | 11 | customer-service 12 | customer-service 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-actuator 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-data-jpa 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-thymeleaf 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-validation 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-web 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-websocket 51 | 52 | 53 | 54 | com.mysql 55 | mysql-connector-j 56 | runtime 57 | 58 | 59 | org.projectlombok 60 | lombok 61 | true 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-test 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-compiler-plugin 75 | 76 | 77 | 78 | org.projectlombok 79 | lombok 80 | 81 | 82 | 83 | 84 | 85 | org.springframework.boot 86 | spring-boot-maven-plugin 87 | 88 | 89 | 90 | org.projectlombok 91 | lombok 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/CustomerServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class CustomerServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(CustomerServiceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/aggregate/CustomerAggregate.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.aggregate; 2 | 3 | import com.ivanfranchin.axoneventcommons.customer.CustomerAddedEvent; 4 | import com.ivanfranchin.axoneventcommons.customer.CustomerDeletedEvent; 5 | import com.ivanfranchin.axoneventcommons.customer.CustomerUpdatedEvent; 6 | import com.ivanfranchin.axoneventcommons.util.MyStringUtils; 7 | import com.ivanfranchin.customerservice.command.AddCustomerCommand; 8 | import com.ivanfranchin.customerservice.command.DeleteCustomerCommand; 9 | import com.ivanfranchin.customerservice.command.UpdateCustomerCommand; 10 | import lombok.NoArgsConstructor; 11 | import org.axonframework.commandhandling.CommandHandler; 12 | import org.axonframework.eventsourcing.EventSourcingHandler; 13 | import org.axonframework.modelling.command.AggregateIdentifier; 14 | import org.axonframework.modelling.command.AggregateLifecycle; 15 | import org.axonframework.spring.stereotype.Aggregate; 16 | 17 | @NoArgsConstructor 18 | @Aggregate 19 | public class CustomerAggregate { 20 | 21 | @AggregateIdentifier 22 | private String id; 23 | private String name; 24 | private String address; 25 | 26 | // -- Add Customer 27 | 28 | @CommandHandler 29 | public CustomerAggregate(AddCustomerCommand command) { 30 | AggregateLifecycle.apply(new CustomerAddedEvent(command.getId(), command.getName(), command.getAddress())); 31 | } 32 | 33 | @EventSourcingHandler 34 | public void handle(CustomerAddedEvent event) { 35 | this.id = event.getId(); 36 | this.name = event.getName(); 37 | this.address = event.getAddress(); 38 | } 39 | 40 | // -- Update Customer 41 | 42 | @CommandHandler 43 | public void handle(UpdateCustomerCommand command) { 44 | String newName = MyStringUtils.getTrimmedValueOrElse(command.getName(), this.name); 45 | String newAddress = MyStringUtils.getTrimmedValueOrElse(command.getAddress(), this.address); 46 | AggregateLifecycle.apply(new CustomerUpdatedEvent(command.getId(), newName, newAddress)); 47 | } 48 | 49 | @EventSourcingHandler 50 | public void handle(CustomerUpdatedEvent event) { 51 | this.id = event.getId(); 52 | this.name = event.getName(); 53 | this.address = event.getAddress(); 54 | } 55 | 56 | // -- Delete Customer 57 | 58 | @CommandHandler 59 | public void handle(DeleteCustomerCommand command) { 60 | AggregateLifecycle.apply(new CustomerDeletedEvent(command.getId())); 61 | } 62 | 63 | @EventSourcingHandler 64 | public void handle(CustomerDeletedEvent event) { 65 | AggregateLifecycle.markDeleted(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/command/AddCustomerCommand.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | public class AddCustomerCommand { 12 | 13 | @TargetAggregateIdentifier 14 | private String id; 15 | private String name; 16 | private String address; 17 | } 18 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/command/DeleteCustomerCommand.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | public class DeleteCustomerCommand { 12 | 13 | @TargetAggregateIdentifier 14 | private String id; 15 | } 16 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/command/UpdateCustomerCommand.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | public class UpdateCustomerCommand { 12 | 13 | @TargetAggregateIdentifier 14 | private String id; 15 | private String name; 16 | private String address; 17 | } 18 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/config/AxonConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.config; 2 | 3 | import com.ivanfranchin.customerservice.interceptor.CommandLoggingDispatchInterceptor; 4 | import com.ivanfranchin.customerservice.interceptor.EventLoggingDispatchInterceptor; 5 | import com.thoughtworks.xstream.XStream; 6 | import org.axonframework.commandhandling.CommandBus; 7 | import org.axonframework.eventhandling.EventBus; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | @Configuration 13 | public class AxonConfig { 14 | 15 | @Autowired 16 | public void registerDispatchInterceptor(CommandBus commandBus, EventBus eventBus) { 17 | commandBus.registerDispatchInterceptor(new CommandLoggingDispatchInterceptor()); 18 | eventBus.registerDispatchInterceptor(new EventLoggingDispatchInterceptor()); 19 | } 20 | 21 | // Workaround to avoid the exception "com.thoughtworks.xstream.security.ForbiddenClassException" 22 | // https://stackoverflow.com/questions/70624317/getting-forbiddenclassexception-in-axon-springboot 23 | @Bean 24 | XStream xStream() { 25 | XStream xStream = new XStream(); 26 | xStream.allowTypesByWildcard(new String[]{ 27 | "com.ivanfranchin.**", 28 | "org.hibernate.proxy.pojo.bytebuddy.**" 29 | }); 30 | return xStream; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/config/ErrorAttributesConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.config; 2 | 3 | import org.springframework.boot.web.error.ErrorAttributeOptions; 4 | import org.springframework.boot.web.error.ErrorAttributeOptions.Include; 5 | import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; 6 | import org.springframework.boot.web.servlet.error.ErrorAttributes; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.context.request.WebRequest; 10 | 11 | import java.util.Map; 12 | 13 | @Configuration 14 | public class ErrorAttributesConfig { 15 | 16 | @Bean 17 | ErrorAttributes errorAttributes() { 18 | return new DefaultErrorAttributes() { 19 | @Override 20 | public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { 21 | return super.getErrorAttributes(webRequest, options.including(Include.EXCEPTION, Include.MESSAGE, Include.BINDING_ERRORS)); 22 | } 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.config; 2 | 3 | import io.swagger.v3.oas.models.Components; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Info; 6 | import org.springdoc.core.models.GroupedOpenApi; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class SwaggerConfig { 13 | 14 | @Value("${spring.application.name}") 15 | private String applicationName; 16 | 17 | @Bean 18 | OpenAPI customOpenAPI() { 19 | return new OpenAPI().components(new Components()).info(new Info().title(applicationName)); 20 | } 21 | 22 | @Bean 23 | GroupedOpenApi customApi() { 24 | return GroupedOpenApi.builder().group("api").pathsToMatch("/api/**").build(); 25 | } 26 | 27 | @Bean 28 | GroupedOpenApi actuatorApi() { 29 | return GroupedOpenApi.builder().group("actuator").pathsToMatch("/actuator/**").build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/exception/CustomerNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class CustomerNotFoundException extends RuntimeException { 8 | 9 | public CustomerNotFoundException(String id) { 10 | super(String.format("Customer with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/interceptor/CommandLoggingDispatchInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.interceptor; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.axonframework.commandhandling.CommandMessage; 5 | import org.axonframework.messaging.MessageDispatchInterceptor; 6 | 7 | import java.util.List; 8 | import java.util.function.BiFunction; 9 | 10 | @Slf4j 11 | public class CommandLoggingDispatchInterceptor implements MessageDispatchInterceptor> { 12 | 13 | @Override 14 | public BiFunction, CommandMessage> handle(List> messages) { 15 | return (index, command) -> { 16 | log.info("[C]=> Dispatching a command: {}.", command); 17 | return command; 18 | }; 19 | } 20 | } -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/interceptor/EventLoggingDispatchInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.interceptor; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.axonframework.eventhandling.EventMessage; 5 | import org.axonframework.messaging.MessageDispatchInterceptor; 6 | 7 | import java.util.List; 8 | import java.util.function.BiFunction; 9 | 10 | @Slf4j 11 | public class EventLoggingDispatchInterceptor implements MessageDispatchInterceptor> { 12 | 13 | @Override 14 | public BiFunction, EventMessage> handle(List> messages) { 15 | return (index, event) -> { 16 | log.info("[E]=> Publishing an event: {}.", event); 17 | return event; 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/model/Customer.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.OneToMany; 7 | import jakarta.persistence.Table; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | 12 | import java.util.LinkedHashSet; 13 | import java.util.Set; 14 | 15 | @Data 16 | @ToString(exclude = "orders") 17 | @EqualsAndHashCode(exclude = "orders") 18 | @Entity 19 | @Table(name = "customers") 20 | public class Customer { 21 | 22 | @Id 23 | private String id; 24 | private String name; 25 | private String address; 26 | 27 | @OneToMany(mappedBy = "customer", cascade = CascadeType.ALL, orphanRemoval = true) 28 | private Set orders = new LinkedHashSet<>(); 29 | } 30 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/model/Order.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.model; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.FetchType; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.JoinColumn; 7 | import jakarta.persistence.ManyToOne; 8 | import jakarta.persistence.Table; 9 | import lombok.Data; 10 | import lombok.EqualsAndHashCode; 11 | import lombok.ToString; 12 | import org.hibernate.annotations.JdbcTypeCode; 13 | import org.hibernate.type.SqlTypes; 14 | 15 | import java.math.BigDecimal; 16 | import java.time.ZonedDateTime; 17 | import java.util.Set; 18 | 19 | @Data 20 | @ToString(exclude = "customer") 21 | @EqualsAndHashCode(exclude = "customer") 22 | @Entity 23 | @Table(name = "orders") 24 | public class Order { 25 | 26 | @Id 27 | private String id; 28 | private String restaurantName; 29 | private String status; 30 | private BigDecimal total = BigDecimal.ZERO; 31 | private ZonedDateTime createdAt; 32 | 33 | @ManyToOne(fetch = FetchType.LAZY) 34 | @JoinColumn(name = "customer_id") 35 | private Customer customer; 36 | 37 | @JdbcTypeCode(SqlTypes.JSON) 38 | private Set items; 39 | } 40 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/model/OrderItem.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | import java.math.BigDecimal; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class OrderItem implements Serializable { 14 | 15 | private String dishName; 16 | private BigDecimal dishPrice; 17 | private Short quantity; 18 | } 19 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/query/GetCustomerOrdersQuery.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.query; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class GetCustomerOrdersQuery { 11 | 12 | private String id; 13 | } 14 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/query/GetCustomerQuery.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.query; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class GetCustomerQuery { 11 | 12 | private String id; 13 | } 14 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/query/GetCustomersQuery.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.query; 2 | 3 | public class GetCustomersQuery { 4 | } 5 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.repository; 2 | 3 | import com.ivanfranchin.customerservice.model.Customer; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface CustomerRepository extends JpaRepository { 9 | } 10 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/repository/CustomerRepositoryProjector.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.repository; 2 | 3 | import com.ivanfranchin.axoneventcommons.customer.CustomerAddedEvent; 4 | import com.ivanfranchin.axoneventcommons.customer.CustomerDeletedEvent; 5 | import com.ivanfranchin.axoneventcommons.customer.CustomerUpdatedEvent; 6 | import com.ivanfranchin.axoneventcommons.order.OrderCreatedEvent; 7 | import com.ivanfranchin.customerservice.model.Customer; 8 | import com.ivanfranchin.customerservice.model.Order; 9 | import com.ivanfranchin.customerservice.model.OrderItem; 10 | import com.ivanfranchin.customerservice.query.GetCustomerOrdersQuery; 11 | import com.ivanfranchin.customerservice.query.GetCustomerQuery; 12 | import com.ivanfranchin.customerservice.exception.CustomerNotFoundException; 13 | import com.ivanfranchin.customerservice.query.GetCustomersQuery; 14 | import lombok.RequiredArgsConstructor; 15 | import lombok.extern.slf4j.Slf4j; 16 | import org.axonframework.eventhandling.EventHandler; 17 | import org.axonframework.queryhandling.QueryHandler; 18 | import org.springframework.stereotype.Service; 19 | 20 | import java.util.List; 21 | import java.util.stream.Collectors; 22 | 23 | @Slf4j 24 | @RequiredArgsConstructor 25 | @Service 26 | public class CustomerRepositoryProjector { 27 | 28 | private final CustomerRepository customerRepository; 29 | private final OrderRepository orderRepository; 30 | 31 | @QueryHandler 32 | public List handle(GetCustomersQuery query) { 33 | return customerRepository.findAll(); 34 | } 35 | 36 | @QueryHandler 37 | public Customer handle(GetCustomerQuery query) { 38 | return customerRepository.findById(query.getId()) 39 | .orElseThrow(() -> new CustomerNotFoundException(query.getId())); 40 | } 41 | 42 | @QueryHandler 43 | public List handle(GetCustomerOrdersQuery query) { 44 | return orderRepository.findByCustomerIdOrderByCreatedAtDesc(query.getId()); 45 | } 46 | 47 | @EventHandler 48 | public void handle(CustomerAddedEvent event) { 49 | log.info("<=[E] Received an event: {}", event); 50 | Customer customer = new Customer(); 51 | customer.setId(event.getId()); 52 | customer.setName(event.getName()); 53 | customer.setAddress(event.getAddress()); 54 | customerRepository.save(customer); 55 | } 56 | 57 | @EventHandler 58 | public void handle(CustomerUpdatedEvent event) { 59 | log.info("<=[E] Received an event: {}", event); 60 | customerRepository.findById(event.getId()).ifPresent(c -> { 61 | c.setName(event.getName()); 62 | c.setAddress(event.getAddress()); 63 | customerRepository.save(c); 64 | }); 65 | } 66 | 67 | @EventHandler 68 | public void handle(CustomerDeletedEvent event) { 69 | log.info("<=[E] Received an event: {}", event); 70 | customerRepository.findById(event.getId()).ifPresent(customerRepository::delete); 71 | } 72 | 73 | // -- Order Events 74 | 75 | @EventHandler 76 | public void handle(OrderCreatedEvent event) { 77 | log.info("<=[E] Received an event: {}", event); 78 | customerRepository.findById(event.getCustomerId()).ifPresent(c -> { 79 | Order order = new Order(); 80 | order.setId(event.getId()); 81 | order.setRestaurantName(event.getRestaurantName()); 82 | order.setStatus(event.getStatus()); 83 | order.setTotal(event.getTotal()); 84 | order.setCreatedAt(event.getCreatedAt()); 85 | order.setItems(event.getItems().stream() 86 | .map(i -> new OrderItem(i.getDishName(), i.getDishPrice(), i.getQuantity())) 87 | .collect(Collectors.toSet())); 88 | order.setCustomer(c); 89 | c.getOrders().add(order); 90 | customerRepository.save(c); 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.repository; 2 | 3 | import com.ivanfranchin.customerservice.model.Order; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.List; 8 | 9 | @Repository 10 | public interface OrderRepository extends JpaRepository { 11 | 12 | List findByCustomerIdOrderByCreatedAtDesc(String customerId); 13 | } 14 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/rest/CustomerController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.rest; 2 | 3 | import com.ivanfranchin.customerservice.command.AddCustomerCommand; 4 | import com.ivanfranchin.customerservice.command.DeleteCustomerCommand; 5 | import com.ivanfranchin.customerservice.command.UpdateCustomerCommand; 6 | import com.ivanfranchin.customerservice.model.Customer; 7 | import com.ivanfranchin.customerservice.model.Order; 8 | import com.ivanfranchin.customerservice.query.GetCustomerOrdersQuery; 9 | import com.ivanfranchin.customerservice.query.GetCustomerQuery; 10 | import com.ivanfranchin.customerservice.query.GetCustomersQuery; 11 | import com.ivanfranchin.customerservice.rest.dto.AddCustomerRequest; 12 | import com.ivanfranchin.customerservice.rest.dto.CustomerOrderResponse; 13 | import com.ivanfranchin.customerservice.rest.dto.CustomerResponse; 14 | import com.ivanfranchin.customerservice.rest.dto.UpdateCustomerRequest; 15 | import jakarta.validation.Valid; 16 | import lombok.RequiredArgsConstructor; 17 | import org.axonframework.commandhandling.gateway.CommandGateway; 18 | import org.axonframework.messaging.responsetypes.ResponseTypes; 19 | import org.axonframework.queryhandling.QueryGateway; 20 | import org.springframework.http.HttpStatus; 21 | import org.springframework.web.bind.annotation.DeleteMapping; 22 | import org.springframework.web.bind.annotation.GetMapping; 23 | import org.springframework.web.bind.annotation.PatchMapping; 24 | import org.springframework.web.bind.annotation.PathVariable; 25 | import org.springframework.web.bind.annotation.PostMapping; 26 | import org.springframework.web.bind.annotation.RequestBody; 27 | import org.springframework.web.bind.annotation.RequestMapping; 28 | import org.springframework.web.bind.annotation.ResponseStatus; 29 | import org.springframework.web.bind.annotation.RestController; 30 | 31 | import java.util.List; 32 | import java.util.UUID; 33 | import java.util.concurrent.CompletableFuture; 34 | 35 | @RequiredArgsConstructor 36 | @RestController 37 | @RequestMapping("/api/customers") 38 | public class CustomerController { 39 | 40 | private final CommandGateway commandGateway; 41 | private final QueryGateway queryGateway; 42 | 43 | @GetMapping 44 | public CompletableFuture> getCustomers() { 45 | return queryGateway.query(new GetCustomersQuery(), ResponseTypes.multipleInstancesOf(Customer.class)) 46 | .thenApply(customers -> customers.stream().map(CustomerResponse::from).toList()); 47 | } 48 | 49 | @GetMapping("/{id}") 50 | public CompletableFuture getCustomer(@PathVariable String id) { 51 | return queryGateway.query(new GetCustomerQuery(id), Customer.class).thenApply(CustomerResponse::from); 52 | } 53 | 54 | @ResponseStatus(HttpStatus.CREATED) 55 | @PostMapping 56 | public CompletableFuture addCustomer(@Valid @RequestBody AddCustomerRequest request) { 57 | return commandGateway.send(new AddCustomerCommand(UUID.randomUUID().toString(), request.name(), request.address())); 58 | } 59 | 60 | @PatchMapping("/{id}") 61 | public CompletableFuture updateCustomer(@PathVariable String id, 62 | @Valid @RequestBody UpdateCustomerRequest request) { 63 | return commandGateway.send(new UpdateCustomerCommand(id, request.name(), request.address())); 64 | } 65 | 66 | @DeleteMapping("/{id}") 67 | public CompletableFuture deleteCustomer(@PathVariable String id) { 68 | return commandGateway.send(new DeleteCustomerCommand(id)); 69 | } 70 | 71 | @GetMapping("/{id}/orders") 72 | public CompletableFuture> getCustomerOrders(@PathVariable String id) { 73 | return queryGateway.query(new GetCustomerOrdersQuery(id), ResponseTypes.multipleInstancesOf(Order.class)) 74 | .thenApply(orders -> orders.stream().map(CustomerOrderResponse::from).toList()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/rest/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.rest; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class IndexController { 8 | 9 | @GetMapping("/") 10 | public String index() { 11 | return "index"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/rest/dto/AddCustomerRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record AddCustomerRequest( 7 | @Schema(example = "Ivan Franchin") @NotBlank String name, 8 | @Schema(example = "Brooklyn 12, NYC") @NotBlank String address) { 9 | } 10 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/rest/dto/CustomerOrderResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.rest.dto; 2 | 3 | import com.ivanfranchin.customerservice.model.Order; 4 | 5 | import java.math.BigDecimal; 6 | import java.time.ZonedDateTime; 7 | import java.util.Set; 8 | import java.util.stream.Collectors; 9 | 10 | public record CustomerOrderResponse(String id, 11 | String restaurantName, 12 | String status, 13 | BigDecimal total, 14 | ZonedDateTime createdAt, 15 | Set items) { 16 | 17 | public record OrderItem(String dishName, BigDecimal dishPrice, Short quantity) { 18 | } 19 | 20 | public static CustomerOrderResponse from(Order order) { 21 | Set items = order.getItems() 22 | .stream() 23 | .map(orderItem -> new OrderItem( 24 | orderItem.getDishName(), 25 | orderItem.getDishPrice(), 26 | orderItem.getQuantity())) 27 | .collect(Collectors.toSet()); 28 | 29 | return new CustomerOrderResponse( 30 | order.getId(), 31 | order.getRestaurantName(), 32 | order.getStatus(), 33 | order.getTotal(), 34 | order.getCreatedAt(), 35 | items); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/rest/dto/CustomerResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.rest.dto; 2 | 3 | import com.ivanfranchin.customerservice.model.Customer; 4 | 5 | public record CustomerResponse(String id, String name, String address) { 6 | 7 | public static CustomerResponse from(Customer customer) { 8 | return new CustomerResponse(customer.getId(), customer.getName(), customer.getAddress()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/rest/dto/UpdateCustomerRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdateCustomerRequest( 6 | @Schema(example = "Ivan Franchin") String name, 7 | @Schema(example = "Bronx 182, NYC") String address) { 8 | } 9 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/websocket/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.websocket; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void registerStompEndpoints(StompEndpointRegistry registry) { 15 | registry.addEndpoint("/websocket").withSockJS(); 16 | } 17 | 18 | @Override 19 | public void configureMessageBroker(MessageBrokerRegistry registry) { 20 | registry.setApplicationDestinationPrefixes("/app"); 21 | registry.enableSimpleBroker("/topic"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /customer-service/src/main/java/com/ivanfranchin/customerservice/websocket/WebSocketHandler.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice.websocket; 2 | 3 | import com.ivanfranchin.axoneventcommons.customer.CustomerAddedEvent; 4 | import com.ivanfranchin.axoneventcommons.customer.CustomerDeletedEvent; 5 | import com.ivanfranchin.axoneventcommons.customer.CustomerUpdatedEvent; 6 | import com.ivanfranchin.axoneventcommons.order.OrderCreatedEvent; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.axonframework.eventhandling.EventHandler; 10 | import org.springframework.messaging.simp.SimpMessagingTemplate; 11 | import org.springframework.stereotype.Service; 12 | 13 | @Slf4j 14 | @RequiredArgsConstructor 15 | @Service 16 | public class WebSocketHandler { 17 | 18 | private final SimpMessagingTemplate simpMessagingTemplate; 19 | 20 | @EventHandler 21 | public void handle(CustomerAddedEvent event) { 22 | log.info("<=[E] Received an event: {}", event); 23 | simpMessagingTemplate.convertAndSend("/topic/customer/added", event); 24 | } 25 | 26 | @EventHandler 27 | public void handle(CustomerUpdatedEvent event) { 28 | log.info("<=[E] Received an event: {}", event); 29 | simpMessagingTemplate.convertAndSend("/topic/customer/updated", event); 30 | } 31 | 32 | @EventHandler 33 | public void handle(CustomerDeletedEvent event) { 34 | log.info("<=[E] Received an event: {}", event); 35 | simpMessagingTemplate.convertAndSend("/topic/customer/deleted", event); 36 | } 37 | 38 | @EventHandler 39 | public void handle(OrderCreatedEvent event) { 40 | log.info("<=[E] Received an event: {}", event); 41 | simpMessagingTemplate.convertAndSend("/topic/customer/order/created", event); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /customer-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=customer-service 2 | 3 | spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/customerdb?characterEncoding=UTF-8&serverTimezone=UTC 4 | spring.datasource.username=root 5 | spring.datasource.password=secret 6 | 7 | spring.jpa.hibernate.ddl-auto=update 8 | 9 | spring.main.allow-circular-references=true 10 | 11 | axon.axonserver.servers=${AXON_SERVER_HOST:localhost}:${AXON_SERVER_PORT:8124} 12 | #axon.serializer.general=jackson 13 | 14 | management.endpoints.web.exposure.include=beans,env,health,info,metrics,mappings 15 | management.endpoint.health.show-details=always 16 | 17 | springdoc.show-actuator=true 18 | springdoc.swagger-ui.groups-order=DESC 19 | springdoc.swagger-ui.disable-swagger-default-url=true 20 | -------------------------------------------------------------------------------- /customer-service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ 2 | ___ _ _ ___| |_ ___ _ __ ___ ___ _ __ ___ ___ _ ____ _(_) ___ ___ 3 | / __| | | / __| __/ _ \| '_ ` _ \ / _ \ '__|____/ __|/ _ \ '__\ \ / / |/ __/ _ \ 4 | | (__| |_| \__ \ || (_) | | | | | | __/ | |_____\__ \ __/ | \ V /| | (_| __/ 5 | \___|\__,_|___/\__\___/|_| |_| |_|\___|_| |___/\___|_| \_/ |_|\___\___| 6 | :: Spring Boot :: ${spring-boot.formatted-version} 7 | -------------------------------------------------------------------------------- /customer-service/src/main/resources/static/app.js: -------------------------------------------------------------------------------- 1 | const customerServiceApiBaseUrl = "http://localhost:9080/api/customers" 2 | 3 | function connectToWebSocket() { 4 | const socket = new SockJS('/websocket') 5 | const stompClient = Stomp.over(socket) 6 | 7 | stompClient.connect({}, 8 | function (frame) { 9 | console.log('Connected: ' + frame) 10 | $('.connWebSocket').find('i').removeClass('red').addClass('green') 11 | 12 | stompClient.subscribe('/topic/customer/added', function (event) { 13 | addCustomer(JSON.parse(event.body)) 14 | }) 15 | 16 | stompClient.subscribe('/topic/customer/updated', function (event) { 17 | updateCustomer(JSON.parse(event.body)) 18 | }) 19 | 20 | stompClient.subscribe('/topic/customer/deleted', function (event) { 21 | removeCustomer(JSON.parse(event.body)) 22 | }) 23 | 24 | stompClient.subscribe('/topic/customer/order/created', function (event) { 25 | addOrder(JSON.parse(event.body)) 26 | }) 27 | }, 28 | function() { 29 | showModal($('.modal.alert'), 'WebSocket Disconnected', 'WebSocket is disconnected. Maybe, customer-service is down or restarting') 30 | $('.connWebSocket').find('i').removeClass('green').addClass('red') 31 | } 32 | ) 33 | } 34 | 35 | function loadCustomers() { 36 | $.ajax({ 37 | url: customerServiceApiBaseUrl, 38 | contentType: "application/json", 39 | success: function(data, textStatus, jqXHR) { 40 | data.forEach(customer => { 41 | addCustomer(customer) 42 | $.ajax({ 43 | url: customerServiceApiBaseUrl.concat("/", customer.id, "/orders"), 44 | contentType: "application/json", 45 | success: function(data, textStatus, jqXHR) { 46 | data.map(order => { 47 | let orderCopy = { ... order } 48 | orderCopy['customerId'] = customer.id 49 | return orderCopy 50 | }) 51 | .forEach(order => addOrder(order)) 52 | }, 53 | error: function (jqXHR, textStatus, errorThrown) {} 54 | }) 55 | }) 56 | }, 57 | error: function (jqXHR, textStatus, errorThrown) {} 58 | }) 59 | } 60 | 61 | function addCustomer(customer) { 62 | const row = 63 | '
'+ 64 | '
'+ 65 | '
'+ 66 | '
'+ 67 | ''+ 68 | ''+ 69 | '
'+ 70 | '
'+ 71 | '

'+customer.name+'

'+ 72 | '
'+ 73 | '
'+ 74 | '

Address: '+customer.address+'

'+ 75 | '
'+ 76 | '
'+ 77 | '
'+customer.id+'
'+ 78 | '
'+ 79 | '
'+ 80 | ''+ 81 | ''+ 82 | '
'+ 83 | '
'+ 84 | '
' 85 | '
'+ 86 | '
' 87 | $('#customerList').prepend(row) 88 | } 89 | 90 | function getOrderRow(order) { 91 | const items = order.items 92 | .map(item => item.quantity + "x " + item.dishName + " " + accounting.formatMoney(item.dishPrice) + " (" + accounting.formatMoney(item.quantity*item.dishPrice) + ")") 93 | .map(description => '
  • ' + description + '
  • ') 94 | .join('') 95 | return ( 96 | ''+ 97 | ''+order.id+''+ 98 | ''+moment(order.createdAt).format('YYYY-MM-DD HH:mm:ss')+''+ 99 | ''+order.status+''+ 100 | ''+order.restaurantName+''+ 101 | ''+accounting.formatMoney(order.total)+''+ 102 | '
      '+items+'
    '+ 103 | '' 104 | ) 105 | } 106 | 107 | function updateCustomer(customer) { 108 | const $customer = $('#'+customer.id) 109 | $customer.find('h3.id').text(customer.name) 110 | $customer.find('p.address > strong').text(customer.address) 111 | } 112 | 113 | function removeCustomer(customer) { 114 | $('#'+customer.id).remove() 115 | } 116 | 117 | function addOrder(order) { 118 | $('#'+order.customerId).find('tbody').prepend(getOrderRow(order)) 119 | } 120 | 121 | function fillForm(data) { 122 | $('#customerForm input[name="id"]').val(data.id) 123 | $('#customerForm input[name="name"]').val(data.name) 124 | $('#customerForm input[name="address"]').val(data.address) 125 | } 126 | 127 | function resetForm() { 128 | $('#customerForm input[name="id"]').val('') 129 | $('#customerForm input[name="name"]').val('') 130 | $('#customerForm input[name="address"]').val('') 131 | } 132 | 133 | function validateAndGetFormData() { 134 | const id = $('#customerForm input[name="id"]').val() 135 | const name = $('#customerForm input[name="name"]').val() 136 | const address = $('#customerForm input[name="address"]').val() 137 | 138 | if (name.trim().length === 0 || address.trim().length === 0) { 139 | showModal($('.modal.alert'), 'Missing fields', 'Please inform customer Name and Address') 140 | return null 141 | } 142 | 143 | return {id, name, address} 144 | } 145 | 146 | function showModal($modal, header, description, fnApprove) { 147 | $modal.find('.header').text(header) 148 | $modal.find('.content').text(description) 149 | $modal.modal({ 150 | onApprove: function() { 151 | fnApprove && fnApprove() 152 | } 153 | }).modal('show') 154 | } 155 | 156 | $(function () { 157 | loadCustomers() 158 | 159 | connectToWebSocket() 160 | 161 | $('#customerForm button[name="btnSave"]').click(function(event) { 162 | event.preventDefault() 163 | const customerData = validateAndGetFormData() 164 | if (customerData == null) { 165 | return 166 | } 167 | 168 | let type = "POST" 169 | let url = customerServiceApiBaseUrl 170 | if (customerData.id.length > 0) { 171 | type = "PATCH" 172 | url = url.concat("/", customerData.id) 173 | } 174 | 175 | $.ajax({ 176 | type, 177 | url, 178 | contentType: "application/json", 179 | data: JSON.stringify({name: customerData.name, address: customerData.address}), 180 | success: function(data, textStatus, jqXHR) { 181 | resetForm() 182 | }, 183 | error: function (jqXHR, textStatus, errorThrown) {} 184 | }) 185 | }) 186 | 187 | $('#customerList').on('click', '.btnDelete', function() { 188 | const $customer = $(this).closest('div.item') 189 | const id = $customer.attr('id') 190 | const name = $customer.find('h3').text() 191 | showModal($('.modal.confirmation'), 'Delete Customer', 'Are you sure you want to delete customer "'+name+'"?', function() { 192 | $.ajax({ 193 | type: "DELETE", 194 | url: customerServiceApiBaseUrl.concat("/", id), 195 | success: function(data, textStatus, jqXHR) {}, 196 | error: function (jqXHR, textStatus, errorThrown) {} 197 | }) 198 | }) 199 | }) 200 | 201 | $('#customerList').on('click', '.btnEdit', function() { 202 | const id = $(this).closest('div.item').attr('id') 203 | $.ajax({ 204 | url: customerServiceApiBaseUrl.concat("/", id), 205 | success: function(data, textStatus, jqXHR) { 206 | fillForm(data) 207 | }, 208 | error: function (jqXHR, textStatus, errorThrown) {} 209 | }) 210 | }) 211 | 212 | $('#customerForm button[name="btnCancel"]').click(function() { 213 | resetForm() 214 | }) 215 | 216 | $('.connWebSocket').click(function() { 217 | connectToWebSocket() 218 | }) 219 | }) 220 | -------------------------------------------------------------------------------- /customer-service/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Customer-Service 6 | 7 | 8 | 9 | 10 | 11 |
    12 | 20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 | 29 | 30 |
    31 |
    32 | 33 | 34 |
    35 |
    36 | 37 | 38 |
    39 |
    40 | 41 | 42 |
    43 |
    44 |
    45 |
    46 |
    47 |
    48 |
    49 | 50 | 51 | 59 | 60 | 61 | 68 |
    69 |
    70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /customer-service/src/test/java/com/ivanfranchin/customerservice/CustomerServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.customerservice; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @Disabled 8 | @SpringBootTest 9 | class CustomerServiceApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | axon-server: 4 | image: 'axoniq/axonserver:4.6.11' 5 | container_name: 'axon-server' 6 | restart: 'unless-stopped' 7 | ports: 8 | - "8024:8024" 9 | - "8124:8124" 10 | 11 | mysql: 12 | image: 'mysql:9.2.0' 13 | container_name: 'mysql' 14 | restart: 'unless-stopped' 15 | ports: 16 | - '3306:3306' 17 | environment: 18 | - 'MYSQL_ROOT_PASSWORD=secret' 19 | - 'MYSQL_DATABASE=customerdb' 20 | healthcheck: 21 | test: "mysqladmin ping -u root -p$${MYSQL_ROOT_PASSWORD}" 22 | 23 | postgres: 24 | container_name: 'postgres' 25 | image: 'postgres:17.2' 26 | restart: 'unless-stopped' 27 | ports: 28 | - '5432:5432' 29 | environment: 30 | - 'POSTGRES_DB=restaurantdb' 31 | - 'POSTGRES_PASSWORD=postgres' 32 | - 'POSTGRES_USER=postgres' 33 | healthcheck: 34 | test: "pg_isready -U postgres" 35 | 36 | mongodb: 37 | image: 'mongo:8.0.6' 38 | container_name: 'mongodb' 39 | restart: 'unless-stopped' 40 | ports: 41 | - '27017:27017' 42 | healthcheck: 43 | test: "echo 'db.stats().ok' | mongosh localhost:27017/foodorderingdb --quiet" 44 | -------------------------------------------------------------------------------- /documentation/axon-server.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/axon-springboot-websocket/a7af07dae6a42fb6fe9d5eba1756691c199cb45b/documentation/axon-server.jpeg -------------------------------------------------------------------------------- /documentation/customer-service.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/axon-springboot-websocket/a7af07dae6a42fb6fe9d5eba1756691c199cb45b/documentation/customer-service.jpeg -------------------------------------------------------------------------------- /documentation/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/axon-springboot-websocket/a7af07dae6a42fb6fe9d5eba1756691c199cb45b/documentation/demo.gif -------------------------------------------------------------------------------- /documentation/food-ordering-service.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/axon-springboot-websocket/a7af07dae6a42fb6fe9d5eba1756691c199cb45b/documentation/food-ordering-service.jpeg -------------------------------------------------------------------------------- /documentation/project-diagram.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/axon-springboot-websocket/a7af07dae6a42fb6fe9d5eba1756691c199cb45b/documentation/project-diagram.jpeg -------------------------------------------------------------------------------- /documentation/restaurant-service.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ivangfr/axon-springboot-websocket/a7af07dae6a42fb6fe9d5eba1756691c199cb45b/documentation/restaurant-service.jpeg -------------------------------------------------------------------------------- /food-ordering-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | axon-springboot-websocket 8 | 1.0.0 9 | ../pom.xml 10 | 11 | food-ordering-service 12 | food-ordering-service 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-actuator 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-data-mongodb 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-thymeleaf 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-validation 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-web 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-websocket 51 | 52 | 53 | 54 | org.projectlombok 55 | lombok 56 | true 57 | 58 | 59 | org.springframework.boot 60 | spring-boot-starter-test 61 | test 62 | 63 | 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-compiler-plugin 70 | 71 | 72 | 73 | org.projectlombok 74 | lombok 75 | 76 | 77 | 78 | 79 | 80 | org.springframework.boot 81 | spring-boot-maven-plugin 82 | 83 | 84 | 85 | org.projectlombok 86 | lombok 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/FoodOrderingServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class FoodOrderingServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(FoodOrderingServiceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/config/AxonConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.config; 2 | 3 | import com.ivanfranchin.foodorderingservice.order.interceptor.CommandLoggingDispatchInterceptor; 4 | import com.ivanfranchin.foodorderingservice.order.interceptor.EventLoggingDispatchInterceptor; 5 | import com.thoughtworks.xstream.XStream; 6 | import org.axonframework.commandhandling.CommandBus; 7 | import org.axonframework.eventhandling.EventBus; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | @Configuration 13 | public class AxonConfig { 14 | 15 | @Autowired 16 | public void registerDispatchInterceptor(CommandBus commandBus, EventBus eventBus) { 17 | commandBus.registerDispatchInterceptor(new CommandLoggingDispatchInterceptor()); 18 | eventBus.registerDispatchInterceptor(new EventLoggingDispatchInterceptor()); 19 | } 20 | 21 | // Workaround to avoid the exception "com.thoughtworks.xstream.security.ForbiddenClassException" 22 | // https://stackoverflow.com/questions/70624317/getting-forbiddenclassexception-in-axon-springboot 23 | @Bean 24 | XStream xStream() { 25 | XStream xStream = new XStream(); 26 | xStream.allowTypesByWildcard(new String[]{ 27 | "com.ivanfranchin.**", 28 | "org.hibernate.proxy.pojo.bytebuddy.**" 29 | }); 30 | return xStream; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/config/ErrorAttributesConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.config; 2 | 3 | import org.springframework.boot.web.error.ErrorAttributeOptions; 4 | import org.springframework.boot.web.error.ErrorAttributeOptions.Include; 5 | import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; 6 | import org.springframework.boot.web.servlet.error.ErrorAttributes; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.context.request.WebRequest; 10 | 11 | import java.util.Map; 12 | 13 | @Configuration 14 | public class ErrorAttributesConfig { 15 | 16 | @Bean 17 | ErrorAttributes errorAttributes() { 18 | return new DefaultErrorAttributes() { 19 | @Override 20 | public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { 21 | return super.getErrorAttributes(webRequest, options.including(Include.EXCEPTION, Include.MESSAGE, Include.BINDING_ERRORS)); 22 | } 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/config/MongoConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.core.convert.converter.Converter; 6 | import org.springframework.data.mongodb.core.convert.MongoCustomConversions; 7 | 8 | import java.time.ZonedDateTime; 9 | import java.util.ArrayList; 10 | import java.util.Date; 11 | import java.util.List; 12 | 13 | import static java.time.ZoneId.systemDefault; 14 | import static java.time.ZonedDateTime.ofInstant; 15 | 16 | @Configuration 17 | public class MongoConfig { 18 | 19 | @Bean 20 | MongoCustomConversions customConversions() { 21 | List> converters = new ArrayList<>(); 22 | converters.add(DateToZonedDateTimeConverter.INSTANCE); 23 | converters.add(ZonedDateTimeToDateConverter.INSTANCE); 24 | return new MongoCustomConversions(converters); 25 | } 26 | 27 | enum DateToZonedDateTimeConverter implements Converter { 28 | 29 | INSTANCE; 30 | 31 | @Override 32 | public ZonedDateTime convert(Date source) { 33 | return ofInstant(source.toInstant(), systemDefault()); 34 | } 35 | } 36 | 37 | enum ZonedDateTimeToDateConverter implements Converter { 38 | 39 | INSTANCE; 40 | 41 | @Override 42 | public Date convert(ZonedDateTime source) { 43 | return Date.from(source.toInstant()); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.config; 2 | 3 | import io.swagger.v3.oas.models.Components; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Info; 6 | import org.springdoc.core.models.GroupedOpenApi; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class SwaggerConfig { 13 | 14 | @Value("${spring.application.name}") 15 | private String applicationName; 16 | 17 | @Bean 18 | OpenAPI customOpenAPI() { 19 | return new OpenAPI().components(new Components()).info(new Info().title(applicationName)); 20 | } 21 | 22 | @Bean 23 | GroupedOpenApi customApi() { 24 | return GroupedOpenApi.builder().group("api").pathsToMatch("/api/**").build(); 25 | } 26 | 27 | @Bean 28 | GroupedOpenApi actuatorApi() { 29 | return GroupedOpenApi.builder().group("actuator").pathsToMatch("/actuator/**").build(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/customer/exception/CustomerNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.customer.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class CustomerNotFoundException extends RuntimeException { 8 | 9 | public CustomerNotFoundException(String id) { 10 | super(String.format("Customer with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/customer/model/Customer.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.customer.model; 2 | 3 | import lombok.Data; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | @Data 8 | @Document(collection = "customers") 9 | public class Customer { 10 | 11 | @Id 12 | private String id; 13 | private String name; 14 | private String address; 15 | } 16 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/customer/repository/CustomerRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.customer.repository; 2 | 3 | import com.ivanfranchin.foodorderingservice.customer.model.Customer; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface CustomerRepository extends MongoRepository { 9 | } 10 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/customer/repository/CustomerRepositoryProjector.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.customer.repository; 2 | 3 | import com.ivanfranchin.axoneventcommons.customer.CustomerAddedEvent; 4 | import com.ivanfranchin.axoneventcommons.customer.CustomerDeletedEvent; 5 | import com.ivanfranchin.axoneventcommons.customer.CustomerUpdatedEvent; 6 | import com.ivanfranchin.foodorderingservice.customer.model.Customer; 7 | import lombok.RequiredArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.axonframework.eventhandling.EventHandler; 10 | import org.springframework.stereotype.Service; 11 | 12 | @Slf4j 13 | @RequiredArgsConstructor 14 | @Service 15 | public class CustomerRepositoryProjector { 16 | 17 | private final CustomerRepository customerRepository; 18 | 19 | @EventHandler 20 | public void handle(CustomerAddedEvent event) { 21 | log.info("<=[E] Received an event: {}", event); 22 | Customer customer = new Customer(); 23 | customer.setId(event.getId()); 24 | customer.setName(event.getName()); 25 | customer.setAddress(event.getAddress()); 26 | customerRepository.save(customer); 27 | } 28 | 29 | @EventHandler 30 | public void handle(CustomerUpdatedEvent event) { 31 | log.info("<=[E] Received an event: {}", event); 32 | customerRepository.findById(event.getId()).ifPresent(c -> { 33 | c.setName(event.getName() == null ? c.getName() : event.getName()); 34 | c.setAddress(event.getAddress() == null ? c.getAddress() : event.getAddress()); 35 | customerRepository.save(c); 36 | }); 37 | } 38 | 39 | @EventHandler 40 | public void handle(CustomerDeletedEvent event) { 41 | log.info("<=[E] Received an event: {}", event); 42 | customerRepository.findById(event.getId()).ifPresent(customerRepository::delete); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/customer/rest/CustomerController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.customer.rest; 2 | 3 | import com.ivanfranchin.foodorderingservice.customer.rest.dto.CustomerResponse; 4 | import com.ivanfranchin.foodorderingservice.customer.service.CustomerService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import java.util.List; 11 | 12 | @RequiredArgsConstructor 13 | @RestController 14 | @RequestMapping("/api/customers") 15 | public class CustomerController { 16 | 17 | private final CustomerService customerService; 18 | 19 | @GetMapping 20 | public List getCustomers() { 21 | return customerService.getCustomers().stream().map(CustomerResponse::from).toList(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/customer/rest/dto/CustomerResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.customer.rest.dto; 2 | 3 | import com.ivanfranchin.foodorderingservice.customer.model.Customer; 4 | 5 | public record CustomerResponse(String id, String name, String address) { 6 | 7 | public static CustomerResponse from(Customer customer) { 8 | return new CustomerResponse(customer.getId(), customer.getName(), customer.getAddress()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/customer/service/CustomerService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.customer.service; 2 | 3 | import com.ivanfranchin.foodorderingservice.customer.model.Customer; 4 | 5 | import java.util.List; 6 | 7 | public interface CustomerService { 8 | 9 | Customer validateAndGetCustomer(String id); 10 | 11 | List getCustomers(); 12 | } 13 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/customer/service/CustomerServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.customer.service; 2 | 3 | import com.ivanfranchin.foodorderingservice.customer.repository.CustomerRepository; 4 | import com.ivanfranchin.foodorderingservice.customer.exception.CustomerNotFoundException; 5 | import com.ivanfranchin.foodorderingservice.customer.model.Customer; 6 | import lombok.RequiredArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.List; 10 | 11 | @RequiredArgsConstructor 12 | @Service 13 | public class CustomerServiceImpl implements CustomerService { 14 | 15 | private final CustomerRepository customerRepository; 16 | 17 | @Override 18 | public Customer validateAndGetCustomer(String id) { 19 | return customerRepository.findById(id).orElseThrow(() -> new CustomerNotFoundException(id)); 20 | } 21 | 22 | @Override 23 | public List getCustomers() { 24 | return customerRepository.findAll(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/aggregate/OrderAggregate.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.aggregate; 2 | 3 | import com.ivanfranchin.axoneventcommons.order.OrderCreatedEvent; 4 | import com.ivanfranchin.foodorderingservice.order.command.CreateOrderCommand; 5 | import com.ivanfranchin.foodorderingservice.order.model.OrderItem; 6 | import com.ivanfranchin.foodorderingservice.order.model.OrderStatus; 7 | import lombok.NoArgsConstructor; 8 | import org.axonframework.commandhandling.CommandHandler; 9 | import org.axonframework.eventsourcing.EventSourcingHandler; 10 | import org.axonframework.modelling.command.AggregateIdentifier; 11 | import org.axonframework.modelling.command.AggregateLifecycle; 12 | import org.axonframework.spring.stereotype.Aggregate; 13 | 14 | import java.math.BigDecimal; 15 | import java.time.ZonedDateTime; 16 | import java.util.LinkedHashSet; 17 | import java.util.Set; 18 | import java.util.stream.Collectors; 19 | 20 | @NoArgsConstructor 21 | @Aggregate 22 | public class OrderAggregate { 23 | 24 | @AggregateIdentifier 25 | private String id; 26 | private OrderStatus status; 27 | private BigDecimal total; 28 | private ZonedDateTime createdAt; 29 | private String customerId; 30 | private String customerName; 31 | private String customerAddress; 32 | private String restaurantId; 33 | private String restaurantName; 34 | private Set items; 35 | 36 | @CommandHandler 37 | public OrderAggregate(CreateOrderCommand command) { 38 | Set evtItems = new LinkedHashSet<>(); 39 | BigDecimal bdTotal = BigDecimal.ZERO; 40 | for (OrderItem orderItem : command.getItems()) { 41 | evtItems.add(new OrderCreatedEvent.OrderItem(orderItem.getDishId(), orderItem.getDishName(), 42 | orderItem.getDishPrice(), orderItem.getQuantity())); 43 | bdTotal = bdTotal.add(orderItem.getDishPrice().multiply(BigDecimal.valueOf(orderItem.getQuantity()))); 44 | } 45 | 46 | AggregateLifecycle.apply(new OrderCreatedEvent(command.getId(), command.getCustomerId(), 47 | command.getCustomerName(), command.getCustomerAddress(), command.getRestaurantId(), 48 | command.getRestaurantName(), OrderStatus.CREATED.name(), bdTotal, ZonedDateTime.now(), evtItems)); 49 | } 50 | 51 | @EventSourcingHandler 52 | public void handle(OrderCreatedEvent event) { 53 | this.id = event.getId(); 54 | this.status = OrderStatus.valueOf(event.getStatus()); 55 | this.total = event.getTotal(); 56 | this.createdAt = event.getCreatedAt(); 57 | this.customerId = event.getCustomerId(); 58 | this.customerName = event.getCustomerName(); 59 | this.customerAddress = event.getCustomerAddress(); 60 | this.restaurantId = event.getRestaurantId(); 61 | this.restaurantName = event.getRestaurantName(); 62 | this.items = event.getItems().stream() 63 | .map(i -> new OrderItem(i.getDishId(), i.getDishName(), i.getDishPrice(), i.getQuantity())) 64 | .collect(Collectors.toSet()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/command/CreateOrderCommand.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.command; 2 | 3 | import com.ivanfranchin.foodorderingservice.order.model.OrderItem; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 8 | 9 | import java.util.Set; 10 | 11 | @Data 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class CreateOrderCommand { 15 | 16 | @TargetAggregateIdentifier 17 | private String id; 18 | private String customerId; 19 | private String customerName; 20 | private String customerAddress; 21 | private String restaurantId; 22 | private String restaurantName; 23 | private Set items; 24 | } 25 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/exception/OrderNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class OrderNotFoundException extends RuntimeException { 8 | 9 | public OrderNotFoundException(String id) { 10 | super(String.format("Order with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/interceptor/CommandLoggingDispatchInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.interceptor; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.axonframework.commandhandling.CommandMessage; 5 | import org.axonframework.messaging.MessageDispatchInterceptor; 6 | 7 | import java.util.List; 8 | import java.util.function.BiFunction; 9 | 10 | @Slf4j 11 | public class CommandLoggingDispatchInterceptor implements MessageDispatchInterceptor> { 12 | 13 | @Override 14 | public BiFunction, CommandMessage> handle(List> messages) { 15 | return (index, command) -> { 16 | log.info("[C]=> Dispatching a command: {}.", command); 17 | return command; 18 | }; 19 | } 20 | } -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/interceptor/EventLoggingDispatchInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.interceptor; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.axonframework.eventhandling.EventMessage; 5 | import org.axonframework.messaging.MessageDispatchInterceptor; 6 | 7 | import java.util.List; 8 | import java.util.function.BiFunction; 9 | 10 | @Slf4j 11 | public class EventLoggingDispatchInterceptor implements MessageDispatchInterceptor> { 12 | 13 | @Override 14 | public BiFunction, EventMessage> handle(List> messages) { 15 | return (index, event) -> { 16 | log.info("[E]=> Publishing an event: {}.", event); 17 | return event; 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/model/Order.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.model; 2 | 3 | import lombok.Data; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | import java.math.BigDecimal; 8 | import java.time.ZonedDateTime; 9 | import java.util.Set; 10 | 11 | @Data 12 | @Document(collection = "orders") 13 | public class Order { 14 | 15 | @Id 16 | private String id; 17 | private OrderStatus status; 18 | private BigDecimal total = BigDecimal.ZERO; 19 | private ZonedDateTime createdAt; 20 | private String customerId; 21 | private String customerName; 22 | private String customerAddress; 23 | private String restaurantId; 24 | private String restaurantName; 25 | private Set items; 26 | } 27 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/model/OrderItem.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.math.BigDecimal; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class OrderItem { 13 | 14 | private String dishId; 15 | private String dishName; 16 | private BigDecimal dishPrice; 17 | private Short quantity; 18 | } 19 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/model/OrderStatus.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.model; 2 | 3 | public enum OrderStatus { 4 | 5 | CREATED, CONFIRMED, CANCELLED 6 | } 7 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/query/GetOrderQuery.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.query; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class GetOrderQuery { 11 | 12 | private String id; 13 | } 14 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/query/GetOrdersQuery.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.query; 2 | 3 | public class GetOrdersQuery { 4 | } 5 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.repository; 2 | 3 | import com.ivanfranchin.foodorderingservice.order.model.Order; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface OrderRepository extends MongoRepository { 9 | } 10 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/repository/OrderRepositoryProjector.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.repository; 2 | 3 | import com.ivanfranchin.axoneventcommons.order.OrderCreatedEvent; 4 | import com.ivanfranchin.foodorderingservice.order.exception.OrderNotFoundException; 5 | import com.ivanfranchin.foodorderingservice.order.model.Order; 6 | import com.ivanfranchin.foodorderingservice.order.model.OrderItem; 7 | import com.ivanfranchin.foodorderingservice.order.model.OrderStatus; 8 | import com.ivanfranchin.foodorderingservice.order.query.GetOrderQuery; 9 | import com.ivanfranchin.foodorderingservice.order.query.GetOrdersQuery; 10 | import lombok.RequiredArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.axonframework.eventhandling.EventHandler; 13 | import org.axonframework.queryhandling.QueryHandler; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.List; 17 | import java.util.stream.Collectors; 18 | 19 | @Slf4j 20 | @RequiredArgsConstructor 21 | @Service 22 | public class OrderRepositoryProjector { 23 | 24 | private final OrderRepository orderRepository; 25 | 26 | @QueryHandler 27 | public List handle(GetOrdersQuery query) { 28 | return orderRepository.findAll(); 29 | } 30 | 31 | @QueryHandler 32 | public Order handle(GetOrderQuery query) { 33 | return orderRepository.findById(query.getId()).orElseThrow(() -> new OrderNotFoundException(query.getId())); 34 | } 35 | 36 | @EventHandler 37 | public void handle(OrderCreatedEvent event) { 38 | log.info("<=[E] Received an event: {}", event); 39 | Order order = new Order(); 40 | order.setId(event.getId()); 41 | order.setCustomerId(event.getCustomerId()); 42 | order.setCustomerName(event.getCustomerName()); 43 | order.setCustomerAddress(event.getCustomerAddress()); 44 | order.setRestaurantId(event.getRestaurantId()); 45 | order.setRestaurantName(event.getRestaurantName()); 46 | order.setStatus(OrderStatus.valueOf(event.getStatus())); 47 | order.setTotal(event.getTotal()); 48 | order.setCreatedAt(event.getCreatedAt()); 49 | order.setItems(event.getItems().stream() 50 | .map(i -> new OrderItem(i.getDishId(), i.getDishName(), i.getDishPrice(), i.getQuantity())) 51 | .collect(Collectors.toSet())); 52 | orderRepository.save(order); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/rest/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.rest; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class IndexController { 8 | 9 | @GetMapping("/") 10 | public String index() { 11 | return "index"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/rest/OrderController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.rest; 2 | 3 | import com.ivanfranchin.foodorderingservice.customer.model.Customer; 4 | import com.ivanfranchin.foodorderingservice.customer.service.CustomerService; 5 | import com.ivanfranchin.foodorderingservice.order.command.CreateOrderCommand; 6 | import com.ivanfranchin.foodorderingservice.order.model.Order; 7 | import com.ivanfranchin.foodorderingservice.order.model.OrderItem; 8 | import com.ivanfranchin.foodorderingservice.order.query.GetOrderQuery; 9 | import com.ivanfranchin.foodorderingservice.order.query.GetOrdersQuery; 10 | import com.ivanfranchin.foodorderingservice.order.rest.dto.CreateOrderRequest; 11 | import com.ivanfranchin.foodorderingservice.order.rest.dto.OrderResponse; 12 | import com.ivanfranchin.foodorderingservice.restaurant.model.Dish; 13 | import com.ivanfranchin.foodorderingservice.restaurant.model.Restaurant; 14 | import com.ivanfranchin.foodorderingservice.restaurant.service.RestaurantService; 15 | import jakarta.validation.Valid; 16 | import lombok.RequiredArgsConstructor; 17 | import org.axonframework.commandhandling.gateway.CommandGateway; 18 | import org.axonframework.messaging.responsetypes.ResponseTypes; 19 | import org.axonframework.queryhandling.QueryGateway; 20 | import org.springframework.http.HttpStatus; 21 | import org.springframework.web.bind.annotation.GetMapping; 22 | import org.springframework.web.bind.annotation.PathVariable; 23 | import org.springframework.web.bind.annotation.PostMapping; 24 | import org.springframework.web.bind.annotation.RequestBody; 25 | import org.springframework.web.bind.annotation.RequestMapping; 26 | import org.springframework.web.bind.annotation.ResponseStatus; 27 | import org.springframework.web.bind.annotation.RestController; 28 | 29 | import java.util.List; 30 | import java.util.Set; 31 | import java.util.UUID; 32 | import java.util.concurrent.CompletableFuture; 33 | import java.util.stream.Collectors; 34 | 35 | @RequiredArgsConstructor 36 | @RestController 37 | @RequestMapping("/api/orders") 38 | public class OrderController { 39 | 40 | private final CommandGateway commandGateway; 41 | private final QueryGateway queryGateway; 42 | private final CustomerService customerService; 43 | private final RestaurantService restaurantService; 44 | 45 | @GetMapping 46 | public CompletableFuture> getOrders() { 47 | return queryGateway.query(new GetOrdersQuery(), ResponseTypes.multipleInstancesOf(Order.class)) 48 | .thenApply(orders -> orders.stream().map(OrderResponse::from).toList()); 49 | } 50 | 51 | @GetMapping("/{id}") 52 | public CompletableFuture getOrder(@PathVariable UUID id) { 53 | return queryGateway.query(new GetOrderQuery(id.toString()), Order.class) 54 | .thenApply(OrderResponse::from); 55 | } 56 | 57 | @ResponseStatus(HttpStatus.CREATED) 58 | @PostMapping 59 | public CompletableFuture createOrder(@Valid @RequestBody CreateOrderRequest request) { 60 | Customer customer = customerService.validateAndGetCustomer(request.customerId().toString()); 61 | Restaurant restaurant = restaurantService.validateAndGetRestaurant(request.restaurantId().toString()); 62 | Set items = request.items().stream().map(i -> { 63 | Dish dish = restaurantService.validateAndGetRestaurantDish(restaurant.getId(), i.dishId().toString()); 64 | return new OrderItem(dish.getId(), dish.getName(), dish.getPrice(), i.quantity()); 65 | }).collect(Collectors.toSet()); 66 | 67 | String id = UUID.randomUUID().toString(); 68 | return commandGateway.send(new CreateOrderCommand(id, customer.getId(), customer.getName(), 69 | customer.getAddress(), restaurant.getId(), restaurant.getName(), items)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/rest/dto/CreateOrderRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotEmpty; 5 | import jakarta.validation.constraints.NotNull; 6 | 7 | import java.util.Set; 8 | import java.util.UUID; 9 | 10 | public record CreateOrderRequest( 11 | UUID customerId, 12 | UUID restaurantId, 13 | @NotNull @NotEmpty Set items) { 14 | 15 | public record OrderItemRequest( 16 | UUID dishId, 17 | @Schema(example = "2") Short quantity) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/rest/dto/OrderResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.rest.dto; 2 | 3 | import com.ivanfranchin.foodorderingservice.order.model.Order; 4 | import com.ivanfranchin.foodorderingservice.order.model.OrderStatus; 5 | 6 | import java.math.BigDecimal; 7 | import java.time.ZonedDateTime; 8 | import java.util.Set; 9 | import java.util.stream.Collectors; 10 | 11 | public record OrderResponse(String id, 12 | String customerId, 13 | String customerName, 14 | String customerAddress, 15 | String restaurantId, 16 | String restaurantName, 17 | OrderStatus status, 18 | BigDecimal total, 19 | ZonedDateTime createdAt, 20 | Set items) { 21 | 22 | public record OrderItem(String dishId, String dishName, BigDecimal dishPrice, Short quantity) { 23 | } 24 | 25 | public static OrderResponse from(Order order) { 26 | Set items = order.getItems() 27 | .stream() 28 | .map(orderItem -> new OrderItem( 29 | orderItem.getDishId(), 30 | orderItem.getDishName(), 31 | orderItem.getDishPrice(), 32 | orderItem.getQuantity())) 33 | .collect(Collectors.toSet()); 34 | 35 | return new OrderResponse( 36 | order.getId(), 37 | order.getCustomerId(), 38 | order.getCustomerName(), 39 | order.getCustomerAddress(), 40 | order.getRestaurantId(), 41 | order.getRestaurantName(), 42 | order.getStatus(), 43 | order.getTotal(), 44 | order.getCreatedAt(), 45 | items); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/websocket/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.websocket; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void registerStompEndpoints(StompEndpointRegistry registry) { 15 | registry.addEndpoint("/websocket").withSockJS(); 16 | } 17 | 18 | @Override 19 | public void configureMessageBroker(MessageBrokerRegistry registry) { 20 | registry.setApplicationDestinationPrefixes("/app"); 21 | registry.enableSimpleBroker("/topic"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/order/websocket/WebSocketHandler.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.order.websocket; 2 | 3 | import com.ivanfranchin.axoneventcommons.customer.CustomerAddedEvent; 4 | import com.ivanfranchin.axoneventcommons.customer.CustomerDeletedEvent; 5 | import com.ivanfranchin.axoneventcommons.customer.CustomerUpdatedEvent; 6 | import com.ivanfranchin.axoneventcommons.order.OrderCreatedEvent; 7 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantAddedEvent; 8 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDeletedEvent; 9 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishAddedEvent; 10 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishDeletedEvent; 11 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishUpdatedEvent; 12 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantUpdatedEvent; 13 | import lombok.RequiredArgsConstructor; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.axonframework.eventhandling.EventHandler; 16 | import org.springframework.messaging.simp.SimpMessagingTemplate; 17 | import org.springframework.stereotype.Service; 18 | 19 | @Slf4j 20 | @RequiredArgsConstructor 21 | @Service 22 | public class WebSocketHandler { 23 | 24 | private final SimpMessagingTemplate simpMessagingTemplate; 25 | 26 | @EventHandler 27 | public void handle(CustomerAddedEvent event) { 28 | log.info("<=[E] Received an event: {}", event); 29 | simpMessagingTemplate.convertAndSend("/topic/customer/added", event); 30 | } 31 | 32 | @EventHandler 33 | public void handle(CustomerUpdatedEvent event) { 34 | log.info("<=[E] Received an event: {}", event); 35 | simpMessagingTemplate.convertAndSend("/topic/customer/updated", event); 36 | } 37 | 38 | @EventHandler 39 | public void handle(CustomerDeletedEvent event) { 40 | log.info("<=[E] Received an event: {}", event); 41 | simpMessagingTemplate.convertAndSend("/topic/customer/deleted", event); 42 | } 43 | 44 | @EventHandler 45 | public void handle(RestaurantAddedEvent event) { 46 | log.info("<=[E] Received an event: {}", event); 47 | simpMessagingTemplate.convertAndSend("/topic/restaurant/added", event); 48 | } 49 | 50 | @EventHandler 51 | public void handle(RestaurantUpdatedEvent event) { 52 | log.info("<=[E] Received an event: {}", event); 53 | simpMessagingTemplate.convertAndSend("/topic/restaurant/updated", event); 54 | } 55 | 56 | @EventHandler 57 | public void handle(RestaurantDeletedEvent event) { 58 | log.info("<=[E] Received an event: {}", event); 59 | simpMessagingTemplate.convertAndSend("/topic/restaurant/deleted", event); 60 | } 61 | 62 | @EventHandler 63 | public void handle(RestaurantDishAddedEvent event) { 64 | log.info("<=[E] Received an event: {}", event); 65 | simpMessagingTemplate.convertAndSend("/topic/restaurant/dish/added", event); 66 | } 67 | 68 | @EventHandler 69 | public void handle(RestaurantDishUpdatedEvent event) { 70 | log.info("<=[E] Received an event: {}", event); 71 | simpMessagingTemplate.convertAndSend("/topic/restaurant/dish/updated", event); 72 | } 73 | 74 | @EventHandler 75 | public void handle(RestaurantDishDeletedEvent event) { 76 | log.info("<=[E] Received an event: {}", event); 77 | simpMessagingTemplate.convertAndSend("/topic/restaurant/dish/deleted", event); 78 | } 79 | 80 | @EventHandler 81 | public void handle(OrderCreatedEvent event) { 82 | log.info("<=[E] Received an event: {}", event); 83 | simpMessagingTemplate.convertAndSend("/topic/order/created", event); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/restaurant/exception/DishNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.restaurant.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class DishNotFoundException extends RuntimeException { 8 | 9 | public DishNotFoundException(String dishId) { 10 | super(String.format("Dish with id '%s' not found", dishId)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/restaurant/exception/RestaurantNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.restaurant.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class RestaurantNotFoundException extends RuntimeException { 8 | 9 | public RestaurantNotFoundException(String id) { 10 | super(String.format("Restaurant with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/restaurant/model/Dish.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.restaurant.model; 2 | 3 | import lombok.Data; 4 | 5 | import java.math.BigDecimal; 6 | 7 | @Data 8 | public class Dish { 9 | 10 | private String id; 11 | private String name; 12 | private BigDecimal price; 13 | } 14 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/restaurant/model/Restaurant.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.restaurant.model; 2 | 3 | import lombok.Data; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | import java.util.LinkedHashSet; 8 | import java.util.Set; 9 | 10 | @Data 11 | @Document(collection = "restaurants") 12 | public class Restaurant { 13 | 14 | @Id 15 | private String id; 16 | private String name; 17 | private Set dishes = new LinkedHashSet<>(); 18 | } 19 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/restaurant/repository/RestaurantRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.restaurant.repository; 2 | 3 | import com.ivanfranchin.foodorderingservice.restaurant.model.Restaurant; 4 | import org.springframework.data.mongodb.repository.MongoRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface RestaurantRepository extends MongoRepository { 9 | } 10 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/restaurant/repository/RestaurantRepositoryProjector.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.restaurant.repository; 2 | 3 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantAddedEvent; 4 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDeletedEvent; 5 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishAddedEvent; 6 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishDeletedEvent; 7 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishUpdatedEvent; 8 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantUpdatedEvent; 9 | import com.ivanfranchin.foodorderingservice.restaurant.model.Dish; 10 | import com.ivanfranchin.foodorderingservice.restaurant.model.Restaurant; 11 | import lombok.RequiredArgsConstructor; 12 | import lombok.extern.slf4j.Slf4j; 13 | import org.axonframework.eventhandling.EventHandler; 14 | import org.springframework.stereotype.Service; 15 | 16 | @Slf4j 17 | @RequiredArgsConstructor 18 | @Service 19 | public class RestaurantRepositoryProjector { 20 | 21 | private final RestaurantRepository restaurantRepository; 22 | 23 | @EventHandler 24 | public void handle(RestaurantAddedEvent event) { 25 | log.info("<=[E] Received an event: {}", event); 26 | Restaurant restaurant = new Restaurant(); 27 | restaurant.setId(event.getId()); 28 | restaurant.setName(event.getName()); 29 | restaurantRepository.save(restaurant); 30 | } 31 | 32 | @EventHandler 33 | public void handle(RestaurantUpdatedEvent event) { 34 | log.info("<=[E] Received an event: {}", event); 35 | restaurantRepository.findById(event.getId()).ifPresent(r -> { 36 | r.setName(event.getName()); 37 | restaurantRepository.save(r); 38 | }); 39 | } 40 | 41 | @EventHandler 42 | public void handle(RestaurantDeletedEvent event) { 43 | log.info("<=[E] Received an event: {}", event); 44 | restaurantRepository.findById(event.getId()).ifPresent(restaurantRepository::delete); 45 | } 46 | 47 | @EventHandler 48 | public void handle(RestaurantDishAddedEvent event) { 49 | log.info("<=[E] Received an event: {}", event); 50 | restaurantRepository.findById(event.getRestaurantId()).ifPresent(r -> { 51 | Dish dish = new Dish(); 52 | dish.setId(event.getDishId()); 53 | dish.setName(event.getDishName()); 54 | dish.setPrice(event.getDishPrice()); 55 | r.getDishes().add(dish); 56 | restaurantRepository.save(r); 57 | }); 58 | } 59 | 60 | @EventHandler 61 | public void handle(RestaurantDishUpdatedEvent event) { 62 | log.info("<=[E] Received an event: {}", event); 63 | restaurantRepository.findById(event.getRestaurantId()).ifPresent(r -> 64 | r.getDishes().stream().filter(d -> d.getId().equals(event.getDishId())).findAny() 65 | .ifPresent(d -> { 66 | d.setName(event.getDishName()); 67 | d.setPrice(event.getDishPrice()); 68 | restaurantRepository.save(r); 69 | })); 70 | } 71 | 72 | @EventHandler 73 | public void handle(RestaurantDishDeletedEvent event) { 74 | log.info("<=[E] Received an event: {}", event); 75 | restaurantRepository.findById(event.getRestaurantId()).ifPresent(r -> { 76 | r.getDishes().removeIf(d -> d.getId().equals(event.getDishId())); 77 | restaurantRepository.save(r); 78 | }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/restaurant/rest/RestaurantController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.restaurant.rest; 2 | 3 | import com.ivanfranchin.foodorderingservice.restaurant.rest.dto.RestaurantResponse; 4 | import com.ivanfranchin.foodorderingservice.restaurant.service.RestaurantService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import java.util.List; 11 | 12 | @RequiredArgsConstructor 13 | @RestController 14 | @RequestMapping("/api/restaurants") 15 | public class RestaurantController { 16 | 17 | private final RestaurantService restaurantService; 18 | 19 | @GetMapping 20 | public List getRestaurants() { 21 | return restaurantService.getRestaurants().stream().map(RestaurantResponse::from).toList(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/restaurant/rest/dto/RestaurantResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.restaurant.rest.dto; 2 | 3 | import com.ivanfranchin.foodorderingservice.restaurant.model.Restaurant; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | 9 | public record RestaurantResponse(String id, String name, Set dishes) { 10 | 11 | public record Dish(String id, String name, BigDecimal price) { 12 | } 13 | 14 | public static RestaurantResponse from(Restaurant restaurant) { 15 | Set dishes = restaurant.getDishes() 16 | .stream() 17 | .map(dish -> new Dish(dish.getId(), dish.getName(), dish.getPrice())) 18 | .collect(Collectors.toSet()); 19 | 20 | return new RestaurantResponse( 21 | restaurant.getId(), 22 | restaurant.getName(), 23 | dishes); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/restaurant/service/RestaurantService.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.restaurant.service; 2 | 3 | import com.ivanfranchin.foodorderingservice.restaurant.model.Dish; 4 | import com.ivanfranchin.foodorderingservice.restaurant.model.Restaurant; 5 | 6 | import java.util.List; 7 | 8 | public interface RestaurantService { 9 | 10 | Restaurant validateAndGetRestaurant(String id); 11 | 12 | Dish validateAndGetRestaurantDish(String restaurantId, String dishId); 13 | 14 | List getRestaurants(); 15 | } 16 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/java/com/ivanfranchin/foodorderingservice/restaurant/service/RestaurantServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice.restaurant.service; 2 | 3 | import com.ivanfranchin.foodorderingservice.restaurant.exception.DishNotFoundException; 4 | import com.ivanfranchin.foodorderingservice.restaurant.exception.RestaurantNotFoundException; 5 | import com.ivanfranchin.foodorderingservice.restaurant.repository.RestaurantRepository; 6 | import com.ivanfranchin.foodorderingservice.restaurant.model.Dish; 7 | import com.ivanfranchin.foodorderingservice.restaurant.model.Restaurant; 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.List; 12 | 13 | @RequiredArgsConstructor 14 | @Service 15 | public class RestaurantServiceImpl implements RestaurantService { 16 | 17 | private final RestaurantRepository restaurantRepository; 18 | 19 | @Override 20 | public Restaurant validateAndGetRestaurant(String id) { 21 | return restaurantRepository.findById(id).orElseThrow(() -> new RestaurantNotFoundException(id)); 22 | } 23 | 24 | @Override 25 | public Dish validateAndGetRestaurantDish(String restaurantId, String dishId) { 26 | return validateAndGetRestaurant(restaurantId).getDishes().stream() 27 | .filter(d -> d.getId().equals(dishId)).findAny().orElseThrow(() -> new DishNotFoundException(dishId)); 28 | } 29 | 30 | @Override 31 | public List getRestaurants() { 32 | return restaurantRepository.findAll(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=food-ordering-service 2 | 3 | spring.data.mongodb.host=${MONGODB_HOST:localhost} 4 | spring.data.mongodb.port=${MONGODB_PORT:27017} 5 | spring.data.mongodb.database=foodorderingdb 6 | 7 | spring.main.allow-circular-references=true 8 | 9 | axon.axonserver.servers=${AXON_SERVER_HOST:localhost}:${AXON_SERVER_PORT:8124} 10 | #axon.serializer.general=jackson 11 | 12 | management.endpoints.web.exposure.include=beans,env,health,info,metrics,mappings 13 | management.endpoint.health.show-details=always 14 | 15 | springdoc.show-actuator=true 16 | springdoc.swagger-ui.groups-order=DESC 17 | springdoc.swagger-ui.disable-swagger-default-url=true 18 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | __ _ _ _ _ 2 | / _| ___ ___ __| | ___ _ __ __| | ___ _ __(_)_ __ __ _ ___ ___ _ ____ _(_) ___ ___ 3 | | |_ / _ \ / _ \ / _` |_____ / _ \| '__/ _` |/ _ \ '__| | '_ \ / _` |_____/ __|/ _ \ '__\ \ / / |/ __/ _ \ 4 | | _| (_) | (_) | (_| |_____| (_) | | | (_| | __/ | | | | | | (_| |_____\__ \ __/ | \ V /| | (_| __/ 5 | |_| \___/ \___/ \__,_| \___/|_| \__,_|\___|_| |_|_| |_|\__, | |___/\___|_| \_/ |_|\___\___| 6 | |___/ 7 | :: Spring Boot :: ${spring-boot.formatted-version} 8 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/resources/static/app.js: -------------------------------------------------------------------------------- 1 | const foodOrderingServiceApiBaseUrl = "http://localhost:9082/api" 2 | 3 | function connectToWebSocket() { 4 | const socket = new SockJS('/websocket') 5 | const stompClient = Stomp.over(socket) 6 | 7 | stompClient.connect({}, 8 | function (frame) { 9 | console.log('Connected: ' + frame) 10 | $('.connWebSocket').find('i').removeClass('red').addClass('green') 11 | 12 | stompClient.subscribe('/topic/customer/added', function (event) { 13 | addCustomer(JSON.parse(event.body)) 14 | }) 15 | 16 | stompClient.subscribe('/topic/customer/updated', function (event) { 17 | updateCustomer(JSON.parse(event.body)) 18 | }) 19 | 20 | stompClient.subscribe('/topic/customer/deleted', function (event) { 21 | removeCustomer(JSON.parse(event.body)) 22 | }) 23 | 24 | stompClient.subscribe('/topic/restaurant/added', function (event) { 25 | addRestaurant(JSON.parse(event.body)) 26 | }) 27 | 28 | stompClient.subscribe('/topic/restaurant/updated', function (event) { 29 | updateRestaurant(JSON.parse(event.body)) 30 | }) 31 | 32 | stompClient.subscribe('/topic/restaurant/deleted', function (event) { 33 | removeRestaurant(JSON.parse(event.body)) 34 | }) 35 | 36 | stompClient.subscribe('/topic/restaurant/dish/added', function (event) { 37 | addRestaurantDish(JSON.parse(event.body)) 38 | }) 39 | 40 | stompClient.subscribe('/topic/restaurant/dish/updated', function (event) { 41 | updateRestaurantDish(JSON.parse(event.body)) 42 | }) 43 | 44 | stompClient.subscribe('/topic/restaurant/dish/deleted', function (event) { 45 | removeRestaurantDish(JSON.parse(event.body)) 46 | }) 47 | 48 | stompClient.subscribe('/topic/order/created', function (event) { 49 | addOrder(JSON.parse(event.body)) 50 | }) 51 | }, 52 | function() { 53 | showModal($('.modal.alert'), 'WebSocket Disconnected', 'WebSocket is disconnected. Maybe, food-ordering-service is down or restarting') 54 | $('.connWebSocket').find('i').removeClass('green').addClass('red') 55 | } 56 | ) 57 | } 58 | 59 | function loadCustomers() { 60 | $.ajax({ 61 | url: foodOrderingServiceApiBaseUrl.concat("/customers"), 62 | contentType: "application/json", 63 | success: function(data, textStatus, jqXHR) { 64 | data.forEach(customer => { 65 | addCustomer(customer) 66 | }) 67 | }, 68 | error: function (jqXHR, textStatus, errorThrown) {} 69 | }) 70 | } 71 | 72 | function loadRestaurants() { 73 | $.ajax({ 74 | url: foodOrderingServiceApiBaseUrl.concat("/restaurants"), 75 | contentType: "application/json", 76 | success: function(data, textStatus, jqXHR) { 77 | data.forEach(restaurant => { 78 | addRestaurant(restaurant) 79 | restaurant.dishes.map(dish => { 80 | return {restaurantId: restaurant.id, dishId: dish.id, dishName: dish.name, dishPrice: dish.price} 81 | }) 82 | .map(dish => addRestaurantDish(dish)) 83 | }) 84 | }, 85 | error: function (jqXHR, textStatus, errorThrown) {} 86 | }) 87 | } 88 | 89 | function loadOrders() { 90 | $.ajax({ 91 | url: foodOrderingServiceApiBaseUrl.concat("/orders"), 92 | contentType: "application/json", 93 | success: function(data, textStatus, jqXHR) { 94 | data.forEach(order => { 95 | addOrder(order) 96 | }) 97 | }, 98 | error: function (jqXHR, textStatus, errorThrown) {} 99 | }) 100 | } 101 | 102 | function addOrder(order) { 103 | const items = order.items 104 | .map(item => item.quantity + "x " + item.dishName + " " + accounting.formatMoney(item.dishPrice) + " (" + accounting.formatMoney(item.quantity*item.dishPrice) + ")") 105 | .map(description => '
  • ' + description + '
  • ') 106 | .join('') 107 | 108 | const row = 109 | ''+ 110 | ''+order.id+''+ 111 | ''+order.customerName+''+ 112 | ''+order.customerAddress+''+ 113 | ''+order.restaurantName+''+ 114 | ''+order.status+''+ 115 | ''+accounting.formatMoney(order.total)+''+ 116 | ''+moment(order.createdAt).format('YYYY-MM-DD HH:mm:ss')+''+ 117 | '
      '+items+'
    '+ 118 | '' 119 | 120 | $('#orderTable').find('tbody').prepend(row) 121 | } 122 | 123 | function addCustomer(customer) { 124 | $('.ui.dropdown').find('div.menu').append('
    '+customer.name+'
    ') 125 | } 126 | 127 | function updateCustomer(customer) { 128 | $('#'+customer.id).text(customer.name) 129 | } 130 | 131 | function removeCustomer(customer) { 132 | $('#'+customer.id).remove() 133 | } 134 | 135 | function addRestaurant(restaurant) { 136 | const row = 137 | '
    '+ 138 | '

    '+restaurant.name+'

    '+ 139 | '
    '+ 140 | '
    '+ 141 | ''+ 142 | ''+ 143 | ''+ 144 | ''+ 145 | ''+ 146 | ''+ 147 | ''+ 148 | ''+ 149 | ''+ 152 | ''+ 153 | ''+ 154 | '
    '+ 150 | ''+ 151 | '
    '+ 155 | '
    ' 156 | $('.ui.accordion').prepend(row) 157 | } 158 | 159 | function updateRestaurant(restaurant) { 160 | $('#'+restaurant.id+'_title').find('h3').text(restaurant.name) 161 | } 162 | 163 | function removeRestaurant(restaurant) { 164 | $('#'+restaurant.id+'_title').remove() 165 | $('#'+restaurant.id+'_content').remove() 166 | } 167 | 168 | function getRestaurantDishRow(dish) { 169 | return ( 170 | ''+ 171 | ''+ 172 | ''+ 173 | ''+ 174 | ''+dish.dishName+''+ 175 | ''+accounting.formatMoney(dish.dishPrice)+''+ 176 | ''+ 177 | '
    '+ 178 | ''+ 179 | '
    '+ 180 | ''+ 181 | ''+ 182 | '' 183 | ) 184 | } 185 | 186 | function addRestaurantDish(dish) { 187 | $('#'+dish.restaurantId+'_content').find('tbody').prepend(getRestaurantDishRow(dish)) 188 | } 189 | 190 | function updateRestaurantDish(dish) { 191 | const $dish = $('#'+dish.dishId); 192 | $dish.find('td.name').text(dish.dishName) 193 | $dish.find('td.price').text(accounting.formatMoney(dish.dishPrice)) 194 | } 195 | 196 | function removeRestaurantDish(dish) { 197 | $('#'+dish.dishId).remove() 198 | } 199 | 200 | function getOrderRequest($this) { 201 | const customerId = $(".dropdown").dropdown('get value') 202 | 203 | const $restaurantContent = $this.closest('.content') 204 | const restaurantContentId = $restaurantContent.attr('id') 205 | const restaurantId = restaurantContentId.substring(0, restaurantContentId.indexOf('_')) 206 | 207 | const items = [] 208 | $restaurantContent.find('tr.dish').each(function(index, tr) { 209 | const dishChecked = $(tr).find('input[type="checkbox"]').prop('checked') 210 | if (dishChecked) { 211 | const dishId = $(tr).attr('id') 212 | const quantity = $(tr).find('input[type="number"]').val() 213 | items.push({dishId, quantity}) 214 | } 215 | }); 216 | 217 | return { customerId, restaurantId, items } 218 | } 219 | 220 | function validOrderRequest(orderRequest) { 221 | if (orderRequest.customerId.length === 0) { 222 | showModal($('.modal.alert'), 'Select a customer', 'Please select a Customer in the dropbox') 223 | return false 224 | } 225 | if (orderRequest.items.length === 0) { 226 | showModal($('.modal.alert'), 'Choose some dishes', 'Please select some dishes of a restaurant') 227 | return false 228 | } 229 | return true 230 | } 231 | 232 | function handlePreviewTotalOrder($this) { 233 | const $table = $this.closest('table') 234 | 235 | let total = 0 236 | $table.find('tr.dish').each(function(index, tr) { 237 | const $tr = $(tr) 238 | const dishChecked = $tr.find('input[type="checkbox"]').prop('checked') 239 | if (dishChecked) { 240 | const price = accounting.unformat($tr.find('td.price').text()) 241 | const quantity = $tr.find('input[type="number"]').val() 242 | total += price * quantity 243 | } 244 | }); 245 | 246 | const $total = $table.find('.total') 247 | if (total > 0) { 248 | $total.text(accounting.formatMoney(total)) 249 | } else { 250 | $total.text('') 251 | } 252 | } 253 | 254 | function showModal($modal, header, description, fnApprove) { 255 | $modal.find('.header').text(header) 256 | $modal.find('.content').text(description) 257 | $modal.modal({ 258 | onApprove: function() { 259 | fnApprove && fnApprove() 260 | } 261 | }).modal('show') 262 | } 263 | 264 | $(function () { 265 | loadCustomers() 266 | loadRestaurants() 267 | loadOrders() 268 | 269 | connectToWebSocket() 270 | 271 | $('.menu .item').tab() 272 | $('.ui.dropdown').dropdown() 273 | $('.ui.accordion').accordion() 274 | 275 | $('.accordion').on('click', '.btnOrder', function() { 276 | const orderRequest = getOrderRequest($(this)) 277 | if (validOrderRequest(orderRequest)) { 278 | $.ajax({ 279 | type: 'POST', 280 | url: foodOrderingServiceApiBaseUrl.concat("/orders"), 281 | contentType: "application/json", 282 | data: JSON.stringify(orderRequest), 283 | success: function(data, textStatus, jqXHR) { 284 | showModal($('.modal.alert'), 'Order Submitted', 'Order submitted successfully. The order id is "' + data + '"') 285 | }, 286 | error: function (jqXHR, textStatus, errorThrown) {} 287 | }) 288 | } 289 | }) 290 | 291 | $('.accordion').on('change', 'input[type="checkbox"]', function() { 292 | handlePreviewTotalOrder($(this)) 293 | }) 294 | 295 | $('.accordion').on('change', 'input[type="number"]', function() { 296 | handlePreviewTotalOrder($(this)) 297 | }) 298 | 299 | $('.connWebSocket').click(function() { 300 | connectToWebSocket() 301 | }) 302 | }) 303 | -------------------------------------------------------------------------------- /food-ordering-service/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Food-Ordering-Service 6 | 7 | 8 | 9 | 10 | 11 |
    12 | 20 |
    21 |
    22 |
    23 | 24 | 28 | 29 | 30 |
    31 | 32 |
    33 |
    34 | 35 | 36 | 41 |
    42 | 43 |
    44 | 45 | 46 |
    47 |
    48 |
    49 |
    50 | 51 | 52 |
    53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
    IDCustomer NameCustomer AddressRestaurant NameStatusTotalCreatedAtItems
    70 |
    71 | 72 | 73 | 80 |
    81 |
    82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /food-ordering-service/src/test/java/com/ivanfranchin/foodorderingservice/FoodOrderingServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.foodorderingservice; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @Disabled 8 | @SpringBootTest 9 | class FoodOrderingServiceApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM http://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.2 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | if ($env:MAVEN_USER_HOME) { 83 | $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" 84 | } 85 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 86 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 87 | 88 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 89 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 90 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 91 | exit $? 92 | } 93 | 94 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 95 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 96 | } 97 | 98 | # prepare tmp dir 99 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 100 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 101 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 102 | trap { 103 | if ($TMP_DOWNLOAD_DIR.Exists) { 104 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 105 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 106 | } 107 | } 108 | 109 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 110 | 111 | # Download and Install Apache Maven 112 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 113 | Write-Verbose "Downloading from: $distributionUrl" 114 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 115 | 116 | $webclient = New-Object System.Net.WebClient 117 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 118 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 119 | } 120 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 121 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 122 | 123 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 124 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 125 | if ($distributionSha256Sum) { 126 | if ($USE_MVND) { 127 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 128 | } 129 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 130 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 131 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 132 | } 133 | } 134 | 135 | # unzip and move 136 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 137 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 138 | try { 139 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 140 | } catch { 141 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 142 | Write-Error "fail to move MAVEN_HOME" 143 | } 144 | } finally { 145 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 146 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 147 | } 148 | 149 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 150 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 3.4.5 9 | 10 | 11 | com.ivanfranchin 12 | axon-springboot-websocket 13 | 1.0.0 14 | pom 15 | axon-springboot-websocket 16 | Demo project for Spring Boot 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 17 32 | 4.11.2 33 | 1.0.0 34 | 2.8.6 35 | 36 | 37 | 38 | 39 | org.axonframework 40 | axon-spring-boot-starter 41 | 42 | 43 | 44 | 45 | com.ivanfranchin 46 | axon-event-commons 47 | ${axon-event-commons.version} 48 | 49 | 50 | 51 | 52 | org.springdoc 53 | springdoc-openapi-starter-webmvc-ui 54 | ${springdoc-openapi.version} 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.axonframework 62 | axon-bom 63 | ${axon.version} 64 | pom 65 | import 66 | 67 | 68 | 69 | 70 | 71 | axon-event-commons 72 | customer-service 73 | restaurant-service 74 | food-ordering-service 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /remove-docker-images.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker rmi ivanfranchin/customer-service:1.0.0 4 | docker rmi ivanfranchin/restaurant-service:1.0.0 5 | docker rmi ivanfranchin/food-ordering-service:1.0.0 6 | -------------------------------------------------------------------------------- /restaurant-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | com.ivanfranchin 7 | axon-springboot-websocket 8 | 1.0.0 9 | ../pom.xml 10 | 11 | restaurant-service 12 | restaurant-service 13 | Demo project for Spring Boot 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-actuator 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-data-jpa 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-thymeleaf 39 | 40 | 41 | org.springframework.boot 42 | spring-boot-starter-validation 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-starter-web 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-starter-websocket 51 | 52 | 53 | 54 | org.postgresql 55 | postgresql 56 | runtime 57 | 58 | 59 | org.projectlombok 60 | lombok 61 | true 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-test 66 | test 67 | 68 | 69 | 70 | 71 | 72 | 73 | org.apache.maven.plugins 74 | maven-compiler-plugin 75 | 76 | 77 | 78 | org.projectlombok 79 | lombok 80 | 81 | 82 | 83 | 84 | 85 | org.springframework.boot 86 | spring-boot-maven-plugin 87 | 88 | 89 | 90 | org.projectlombok 91 | lombok 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/RestaurantServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class RestaurantServiceApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(RestaurantServiceApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/aggregate/RestaurantAggregate.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.aggregate; 2 | 3 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantAddedEvent; 4 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDeletedEvent; 5 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishAddedEvent; 6 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishDeletedEvent; 7 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishUpdatedEvent; 8 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantUpdatedEvent; 9 | import com.ivanfranchin.axoneventcommons.util.MyStringUtils; 10 | import com.ivanfranchin.restaurantservice.command.AddRestaurantCommand; 11 | import com.ivanfranchin.restaurantservice.command.DeleteRestaurantDishCommand; 12 | import com.ivanfranchin.restaurantservice.exception.DishNotFoundException; 13 | import com.ivanfranchin.restaurantservice.command.AddRestaurantDishCommand; 14 | import com.ivanfranchin.restaurantservice.command.DeleteRestaurantCommand; 15 | import com.ivanfranchin.restaurantservice.command.UpdateRestaurantCommand; 16 | import com.ivanfranchin.restaurantservice.command.UpdateRestaurantDishCommand; 17 | import lombok.AllArgsConstructor; 18 | import lombok.Data; 19 | import lombok.NoArgsConstructor; 20 | import org.axonframework.commandhandling.CommandHandler; 21 | import org.axonframework.eventsourcing.EventSourcingHandler; 22 | import org.axonframework.modelling.command.AggregateIdentifier; 23 | import org.axonframework.modelling.command.AggregateLifecycle; 24 | import org.axonframework.spring.stereotype.Aggregate; 25 | 26 | import java.math.BigDecimal; 27 | import java.util.LinkedHashSet; 28 | import java.util.Set; 29 | 30 | @NoArgsConstructor 31 | @Aggregate 32 | public class RestaurantAggregate { 33 | 34 | @AggregateIdentifier 35 | private String id; 36 | private String name; 37 | private Set dishes; 38 | 39 | // -- Add Restaurant 40 | 41 | @CommandHandler 42 | public RestaurantAggregate(AddRestaurantCommand command) { 43 | AggregateLifecycle.apply(new RestaurantAddedEvent(command.getId(), command.getName())); 44 | } 45 | 46 | @EventSourcingHandler 47 | public void handle(RestaurantAddedEvent event) { 48 | this.id = event.getId(); 49 | this.name = event.getName(); 50 | this.dishes = new LinkedHashSet<>(); 51 | } 52 | 53 | // -- Update Restaurant 54 | 55 | @CommandHandler 56 | public void handle(UpdateRestaurantCommand command) { 57 | String newName = MyStringUtils.getTrimmedValueOrElse(command.getName(), this.name); 58 | AggregateLifecycle.apply(new RestaurantUpdatedEvent(command.getId(), newName)); 59 | } 60 | 61 | @EventSourcingHandler 62 | public void handle(RestaurantUpdatedEvent event) { 63 | this.id = event.getId(); 64 | this.name = event.getName(); 65 | } 66 | 67 | // -- Delete Restaurant 68 | 69 | @CommandHandler 70 | public void handle(DeleteRestaurantCommand command) { 71 | AggregateLifecycle.apply(new RestaurantDeletedEvent(command.getId())); 72 | } 73 | 74 | @EventSourcingHandler 75 | public void handle(RestaurantDeletedEvent event) { 76 | AggregateLifecycle.markDeleted(); 77 | } 78 | 79 | // -- Add Restaurant Dish 80 | 81 | @CommandHandler 82 | public void handle(AddRestaurantDishCommand command) { 83 | AggregateLifecycle.apply(new RestaurantDishAddedEvent(command.getRestaurantId(), command.getDishId(), 84 | command.getDishName(), command.getDishPrice())); 85 | } 86 | 87 | @EventSourcingHandler 88 | public void handle(RestaurantDishAddedEvent event) { 89 | this.id = event.getRestaurantId(); 90 | this.dishes.add(new Dish(event.getDishId(), event.getDishName(), event.getDishPrice())); 91 | } 92 | 93 | // -- Update Restaurant Dish 94 | 95 | @CommandHandler 96 | public void handle(UpdateRestaurantDishCommand command) { 97 | this.dishes.stream().filter(d -> d.getId().equals(command.getDishId())).findAny().ifPresentOrElse(d -> { 98 | String newName = MyStringUtils.getTrimmedValueOrElse(command.getDishName(), d.getName()); 99 | BigDecimal newPrice = command.getDishPrice() == null ? d.getPrice() : command.getDishPrice(); 100 | AggregateLifecycle.apply(new RestaurantDishUpdatedEvent(command.getRestaurantId(), command.getDishId(), newName, newPrice)); 101 | }, () -> { 102 | throw new DishNotFoundException(command.getRestaurantId(), command.getDishId()); 103 | }); 104 | } 105 | 106 | @EventSourcingHandler 107 | public void handle(RestaurantDishUpdatedEvent event) { 108 | this.id = event.getRestaurantId(); 109 | this.dishes.stream().filter(d -> d.getId().equals(event.getDishId())).findAny().ifPresent(d -> { 110 | d.setName(event.getDishName()); 111 | d.setPrice(event.getDishPrice()); 112 | }); 113 | } 114 | 115 | // -- Delete Restaurant Dish 116 | 117 | @CommandHandler 118 | public void handle(DeleteRestaurantDishCommand command) { 119 | if (this.dishes.stream().noneMatch(d -> d.getId().equals(command.getDishId()))) { 120 | throw new DishNotFoundException(command.getRestaurantId(), command.getDishId()); 121 | } 122 | AggregateLifecycle.apply(new RestaurantDishDeletedEvent(command.getRestaurantId(), command.getDishId())); 123 | } 124 | 125 | @EventSourcingHandler 126 | public void handle(RestaurantDishDeletedEvent event) { 127 | this.id = event.getRestaurantId(); 128 | this.dishes.removeIf(d -> d.getId().equals(event.getDishId())); 129 | } 130 | 131 | @Data 132 | @AllArgsConstructor 133 | @NoArgsConstructor 134 | public static class Dish { 135 | 136 | private String id; 137 | private String name; 138 | private BigDecimal price; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/command/AddRestaurantCommand.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | public class AddRestaurantCommand { 12 | 13 | @TargetAggregateIdentifier 14 | private String id; 15 | private String name; 16 | } 17 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/command/AddRestaurantDishCommand.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | import java.math.BigDecimal; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class AddRestaurantDishCommand { 14 | 15 | @TargetAggregateIdentifier 16 | private String restaurantId; 17 | private String dishId; 18 | private String dishName; 19 | private BigDecimal dishPrice; 20 | } 21 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/command/DeleteRestaurantCommand.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | public class DeleteRestaurantCommand { 12 | 13 | @TargetAggregateIdentifier 14 | private String id; 15 | } 16 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/command/DeleteRestaurantDishCommand.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | public class DeleteRestaurantDishCommand { 12 | 13 | @TargetAggregateIdentifier 14 | private String restaurantId; 15 | private String dishId; 16 | } 17 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/command/UpdateRestaurantCommand.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | @Data 9 | @AllArgsConstructor 10 | @NoArgsConstructor 11 | public class UpdateRestaurantCommand { 12 | 13 | @TargetAggregateIdentifier 14 | private String id; 15 | private String name; 16 | } 17 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/command/UpdateRestaurantDishCommand.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | import java.math.BigDecimal; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class UpdateRestaurantDishCommand { 14 | 15 | @TargetAggregateIdentifier 16 | private String restaurantId; 17 | private String dishId; 18 | private String dishName; 19 | private BigDecimal dishPrice; 20 | } 21 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/config/AxonConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.config; 2 | 3 | import com.ivanfranchin.restaurantservice.interceptor.CommandLoggingDispatchInterceptor; 4 | import com.ivanfranchin.restaurantservice.interceptor.EventLoggingDispatchInterceptor; 5 | import com.thoughtworks.xstream.XStream; 6 | import org.axonframework.commandhandling.CommandBus; 7 | import org.axonframework.eventhandling.EventBus; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | @Configuration 13 | public class AxonConfig { 14 | 15 | @Autowired 16 | public void registerDispatchInterceptor(CommandBus commandBus, EventBus eventBus) { 17 | commandBus.registerDispatchInterceptor(new CommandLoggingDispatchInterceptor()); 18 | eventBus.registerDispatchInterceptor(new EventLoggingDispatchInterceptor()); 19 | } 20 | 21 | // Workaround to avoid the exception "com.thoughtworks.xstream.security.ForbiddenClassException" 22 | // https://stackoverflow.com/questions/70624317/getting-forbiddenclassexception-in-axon-springboot 23 | @Bean 24 | XStream xStream() { 25 | XStream xStream = new XStream(); 26 | xStream.allowTypesByWildcard(new String[]{ 27 | "com.ivanfranchin.**", 28 | "org.hibernate.proxy.pojo.bytebuddy.**" 29 | }); 30 | return xStream; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/config/ErrorAttributesConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.config; 2 | 3 | import org.springframework.boot.web.error.ErrorAttributeOptions; 4 | import org.springframework.boot.web.error.ErrorAttributeOptions.Include; 5 | import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; 6 | import org.springframework.boot.web.servlet.error.ErrorAttributes; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.web.context.request.WebRequest; 10 | 11 | import java.util.Map; 12 | 13 | @Configuration 14 | public class ErrorAttributesConfig { 15 | 16 | @Bean 17 | ErrorAttributes errorAttributes() { 18 | return new DefaultErrorAttributes() { 19 | @Override 20 | public Map getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) { 21 | return super.getErrorAttributes(webRequest, options.including(Include.EXCEPTION, Include.MESSAGE, Include.BINDING_ERRORS)); 22 | } 23 | }; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.config; 2 | 3 | import io.swagger.v3.oas.models.Components; 4 | import io.swagger.v3.oas.models.OpenAPI; 5 | import io.swagger.v3.oas.models.info.Info; 6 | import org.springdoc.core.models.GroupedOpenApi; 7 | import org.springframework.beans.factory.annotation.Value; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class SwaggerConfig { 13 | 14 | @Value("${spring.application.name}") 15 | private String applicationName; 16 | 17 | @Bean 18 | OpenAPI customOpenAPI() { 19 | return new OpenAPI().components(new Components()).info(new Info().title(applicationName)); 20 | } 21 | 22 | @Bean 23 | GroupedOpenApi customApi() { 24 | return GroupedOpenApi.builder().group("api").pathsToMatch("/api/**").build(); 25 | } 26 | 27 | @Bean 28 | GroupedOpenApi actuatorApi() { 29 | return GroupedOpenApi.builder().group("actuator").pathsToMatch("/actuator/**").build(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/exception/DishNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class DishNotFoundException extends RuntimeException { 8 | 9 | public DishNotFoundException(String restaurantId, String dishId) { 10 | super(String.format("Dish with id '%s' in restaurant '%s' not found", dishId, restaurantId)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/exception/RestaurantNotFoundException.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.web.bind.annotation.ResponseStatus; 5 | 6 | @ResponseStatus(HttpStatus.NOT_FOUND) 7 | public class RestaurantNotFoundException extends RuntimeException { 8 | 9 | public RestaurantNotFoundException(String id) { 10 | super(String.format("Restaurant with id '%s' not found", id)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/interceptor/CommandLoggingDispatchInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.interceptor; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.axonframework.commandhandling.CommandMessage; 5 | import org.axonframework.messaging.MessageDispatchInterceptor; 6 | 7 | import java.util.List; 8 | import java.util.function.BiFunction; 9 | 10 | @Slf4j 11 | public class CommandLoggingDispatchInterceptor implements MessageDispatchInterceptor> { 12 | 13 | @Override 14 | public BiFunction, CommandMessage> handle(List> messages) { 15 | return (index, command) -> { 16 | log.info("[C]=> Dispatching a command: {}.", command); 17 | return command; 18 | }; 19 | } 20 | } -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/interceptor/EventLoggingDispatchInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.interceptor; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.axonframework.eventhandling.EventMessage; 5 | import org.axonframework.messaging.MessageDispatchInterceptor; 6 | 7 | import java.util.List; 8 | import java.util.function.BiFunction; 9 | 10 | @Slf4j 11 | public class EventLoggingDispatchInterceptor implements MessageDispatchInterceptor> { 12 | 13 | @Override 14 | public BiFunction, EventMessage> handle(List> messages) { 15 | return (index, event) -> { 16 | log.info("[E]=> Publishing an event: {}.", event); 17 | return event; 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/model/Dish.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.model; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.Id; 5 | import jakarta.persistence.JoinColumn; 6 | import jakarta.persistence.ManyToOne; 7 | import jakarta.persistence.Table; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | 12 | import java.math.BigDecimal; 13 | 14 | @Data 15 | @ToString(exclude = "restaurant") 16 | @EqualsAndHashCode(exclude = "restaurant") 17 | @Entity 18 | @Table(name = "dishes") 19 | public class Dish { 20 | 21 | @Id 22 | private String id; 23 | private String name; 24 | private BigDecimal price; 25 | 26 | @ManyToOne 27 | @JoinColumn(name = "restaurant_id") 28 | private Restaurant restaurant; 29 | } 30 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/model/Order.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.model; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.FetchType; 5 | import jakarta.persistence.Id; 6 | import jakarta.persistence.JoinColumn; 7 | import jakarta.persistence.ManyToOne; 8 | import jakarta.persistence.Table; 9 | import lombok.Data; 10 | import lombok.EqualsAndHashCode; 11 | import lombok.ToString; 12 | import org.hibernate.annotations.JdbcTypeCode; 13 | import org.hibernate.type.SqlTypes; 14 | 15 | import java.math.BigDecimal; 16 | import java.time.ZonedDateTime; 17 | import java.util.Set; 18 | 19 | @Data 20 | @ToString(exclude = "restaurant") 21 | @EqualsAndHashCode(exclude = "restaurant") 22 | @Entity 23 | @Table(name = "orders") 24 | public class Order { 25 | 26 | @Id 27 | private String id; 28 | private String customerName; 29 | private String customerAddress; 30 | private String status; 31 | private BigDecimal total = BigDecimal.ZERO; 32 | private ZonedDateTime createdAt; 33 | 34 | @ManyToOne(fetch = FetchType.LAZY) 35 | @JoinColumn(name = "restaurant_id") 36 | private Restaurant restaurant; 37 | 38 | @JdbcTypeCode(SqlTypes.JSON) 39 | private Set items; 40 | } 41 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/model/OrderItem.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | import java.io.Serializable; 8 | import java.math.BigDecimal; 9 | 10 | @Data 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class OrderItem implements Serializable { 14 | 15 | private String dishId; 16 | private String dishName; 17 | private BigDecimal dishPrice; 18 | private Short quantity; 19 | } 20 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/model/Restaurant.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.model; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Entity; 5 | import jakarta.persistence.FetchType; 6 | import jakarta.persistence.Id; 7 | import jakarta.persistence.OneToMany; 8 | import jakarta.persistence.Table; 9 | import lombok.Data; 10 | import lombok.EqualsAndHashCode; 11 | import lombok.ToString; 12 | 13 | import java.util.LinkedHashSet; 14 | import java.util.Set; 15 | 16 | @Data 17 | @ToString(exclude = {"dishes", "orders"}) 18 | @EqualsAndHashCode(exclude = {"dishes", "orders"}) 19 | @Entity 20 | @Table(name = "restaurants") 21 | public class Restaurant { 22 | 23 | @Id 24 | private String id; 25 | private String name; 26 | 27 | @OneToMany(fetch = FetchType.EAGER, mappedBy = "restaurant", cascade = CascadeType.ALL, orphanRemoval = true) 28 | private Set dishes = new LinkedHashSet<>(); 29 | 30 | @OneToMany(mappedBy = "restaurant", cascade = CascadeType.ALL, orphanRemoval = true) 31 | private Set orders = new LinkedHashSet<>(); 32 | } 33 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/query/GetRestaurantDishQuery.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.query; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class GetRestaurantDishQuery { 11 | 12 | private String restaurantId; 13 | private String dishId; 14 | } 15 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/query/GetRestaurantOrdersQuery.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.query; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class GetRestaurantOrdersQuery { 11 | 12 | private String id; 13 | } 14 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/query/GetRestaurantQuery.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.query; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Data; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class GetRestaurantQuery { 11 | 12 | private String id; 13 | } 14 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/query/GetRestaurantsQuery.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.query; 2 | 3 | public class GetRestaurantsQuery { 4 | } 5 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/repository/OrderRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.repository; 2 | 3 | import com.ivanfranchin.restaurantservice.model.Order; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import java.util.List; 8 | 9 | @Repository 10 | public interface OrderRepository extends JpaRepository { 11 | 12 | List findByRestaurantIdOrderByCreatedAtDesc(String restaurantId); 13 | } 14 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/repository/RestaurantRepository.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.repository; 2 | 3 | import com.ivanfranchin.restaurantservice.model.Restaurant; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface RestaurantRepository extends JpaRepository { 9 | } 10 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/repository/RestaurantRepositoryProjector.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.repository; 2 | 3 | import com.ivanfranchin.axoneventcommons.order.OrderCreatedEvent; 4 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantAddedEvent; 5 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDeletedEvent; 6 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishAddedEvent; 7 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishDeletedEvent; 8 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishUpdatedEvent; 9 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantUpdatedEvent; 10 | import com.ivanfranchin.restaurantservice.model.Dish; 11 | import com.ivanfranchin.restaurantservice.model.Order; 12 | import com.ivanfranchin.restaurantservice.model.OrderItem; 13 | import com.ivanfranchin.restaurantservice.model.Restaurant; 14 | import com.ivanfranchin.restaurantservice.query.GetRestaurantDishQuery; 15 | import com.ivanfranchin.restaurantservice.query.GetRestaurantOrdersQuery; 16 | import com.ivanfranchin.restaurantservice.query.GetRestaurantQuery; 17 | import com.ivanfranchin.restaurantservice.query.GetRestaurantsQuery; 18 | import com.ivanfranchin.restaurantservice.exception.DishNotFoundException; 19 | import com.ivanfranchin.restaurantservice.exception.RestaurantNotFoundException; 20 | import lombok.RequiredArgsConstructor; 21 | import lombok.extern.slf4j.Slf4j; 22 | import org.axonframework.eventhandling.EventHandler; 23 | import org.axonframework.queryhandling.QueryHandler; 24 | import org.springframework.stereotype.Service; 25 | 26 | import java.util.List; 27 | import java.util.stream.Collectors; 28 | 29 | @Slf4j 30 | @RequiredArgsConstructor 31 | @Service 32 | public class RestaurantRepositoryProjector { 33 | 34 | private final RestaurantRepository restaurantRepository; 35 | private final OrderRepository orderRepository; 36 | 37 | @QueryHandler 38 | public List handle(GetRestaurantsQuery query) { 39 | return restaurantRepository.findAll(); 40 | } 41 | 42 | @QueryHandler 43 | public Restaurant handle(GetRestaurantQuery query) { 44 | return restaurantRepository.findById(query.getId()) 45 | .orElseThrow(() -> new RestaurantNotFoundException(query.getId())); 46 | } 47 | 48 | @QueryHandler 49 | public Dish handle(GetRestaurantDishQuery query) { 50 | return restaurantRepository.findById(query.getRestaurantId()) 51 | .orElseThrow(() -> new RestaurantNotFoundException(query.getRestaurantId())) 52 | .getDishes() 53 | .stream() 54 | .filter(dish -> dish.getId().equals(query.getDishId())) 55 | .findAny() 56 | .orElseThrow(() -> new DishNotFoundException(query.getRestaurantId(), query.getDishId())); 57 | } 58 | 59 | @QueryHandler 60 | public List handle(GetRestaurantOrdersQuery query) { 61 | return orderRepository.findByRestaurantIdOrderByCreatedAtDesc(query.getId()); 62 | } 63 | 64 | @EventHandler 65 | public void handle(RestaurantAddedEvent event) { 66 | log.info("<=[E] Received an event: {}", event); 67 | Restaurant restaurant = new Restaurant(); 68 | restaurant.setId(event.getId()); 69 | restaurant.setName(event.getName()); 70 | restaurantRepository.save(restaurant); 71 | } 72 | 73 | @EventHandler 74 | public void handle(RestaurantUpdatedEvent event) { 75 | log.info("<=[E] Received an event: {}", event); 76 | restaurantRepository.findById(event.getId()).ifPresent(r -> { 77 | r.setName(event.getName()); 78 | restaurantRepository.save(r); 79 | }); 80 | } 81 | 82 | @EventHandler 83 | public void handle(RestaurantDeletedEvent event) { 84 | log.info("<=[E] Received an event: {}", event); 85 | restaurantRepository.findById(event.getId()).ifPresent(restaurantRepository::delete); 86 | } 87 | 88 | @EventHandler 89 | public void handle(RestaurantDishAddedEvent event) { 90 | log.info("<=[E] Received an event: {}", event); 91 | restaurantRepository.findById(event.getRestaurantId()).ifPresent(r -> { 92 | Dish dish = new Dish(); 93 | dish.setId(event.getDishId()); 94 | dish.setName(event.getDishName()); 95 | dish.setPrice(event.getDishPrice()); 96 | dish.setRestaurant(r); 97 | r.getDishes().add(dish); 98 | restaurantRepository.save(r); 99 | }); 100 | } 101 | 102 | @EventHandler 103 | public void handle(RestaurantDishUpdatedEvent event) { 104 | log.info("<=[E] Received an event: {}", event); 105 | restaurantRepository.findById(event.getRestaurantId()).ifPresent(r -> 106 | r.getDishes().stream().filter(d -> d.getId().equals(event.getDishId())).findAny() 107 | .ifPresent(d -> { 108 | d.setName(event.getDishName()); 109 | d.setPrice(event.getDishPrice()); 110 | restaurantRepository.save(r); 111 | })); 112 | } 113 | 114 | @EventHandler 115 | public void handle(RestaurantDishDeletedEvent event) { 116 | log.info("<=[E] Received an event: {}", event); 117 | restaurantRepository.findById(event.getRestaurantId()).ifPresent(r -> { 118 | r.getDishes().removeIf(d -> d.getId().equals(event.getDishId())); 119 | restaurantRepository.save(r); 120 | }); 121 | } 122 | 123 | // -- Order Events 124 | 125 | @EventHandler 126 | public void handle(OrderCreatedEvent event) { 127 | log.info("<=[E] Received an event: {}", event); 128 | restaurantRepository.findById(event.getRestaurantId()).ifPresent(r -> { 129 | Order order = new Order(); 130 | order.setId(event.getId()); 131 | order.setCustomerName(event.getCustomerName()); 132 | order.setCustomerAddress(event.getCustomerAddress()); 133 | order.setStatus(event.getStatus()); 134 | order.setTotal(event.getTotal()); 135 | order.setCreatedAt(event.getCreatedAt()); 136 | order.setItems(event.getItems().stream() 137 | .map(i -> new OrderItem(i.getDishId(), i.getDishName(), i.getDishPrice(), i.getQuantity())) 138 | .collect(Collectors.toSet())); 139 | order.setRestaurant(r); 140 | r.getOrders().add(order); 141 | restaurantRepository.save(r); 142 | }); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/rest/IndexController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.rest; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class IndexController { 8 | 9 | @GetMapping("/") 10 | public String index() { 11 | return "index"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/rest/RestaurantController.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.rest; 2 | 3 | import com.ivanfranchin.restaurantservice.command.AddRestaurantCommand; 4 | import com.ivanfranchin.restaurantservice.command.AddRestaurantDishCommand; 5 | import com.ivanfranchin.restaurantservice.command.DeleteRestaurantCommand; 6 | import com.ivanfranchin.restaurantservice.command.DeleteRestaurantDishCommand; 7 | import com.ivanfranchin.restaurantservice.command.UpdateRestaurantCommand; 8 | import com.ivanfranchin.restaurantservice.command.UpdateRestaurantDishCommand; 9 | import com.ivanfranchin.restaurantservice.model.Dish; 10 | import com.ivanfranchin.restaurantservice.model.Order; 11 | import com.ivanfranchin.restaurantservice.model.Restaurant; 12 | import com.ivanfranchin.restaurantservice.query.GetRestaurantDishQuery; 13 | import com.ivanfranchin.restaurantservice.query.GetRestaurantOrdersQuery; 14 | import com.ivanfranchin.restaurantservice.query.GetRestaurantQuery; 15 | import com.ivanfranchin.restaurantservice.query.GetRestaurantsQuery; 16 | import com.ivanfranchin.restaurantservice.rest.dto.AddRestaurantDishRequest; 17 | import com.ivanfranchin.restaurantservice.rest.dto.AddRestaurantRequest; 18 | import com.ivanfranchin.restaurantservice.rest.dto.DishResponse; 19 | import com.ivanfranchin.restaurantservice.rest.dto.RestaurantOrderResponse; 20 | import com.ivanfranchin.restaurantservice.rest.dto.RestaurantResponse; 21 | import com.ivanfranchin.restaurantservice.rest.dto.UpdateRestaurantDishRequest; 22 | import com.ivanfranchin.restaurantservice.rest.dto.UpdateRestaurantRequest; 23 | import jakarta.validation.Valid; 24 | import lombok.RequiredArgsConstructor; 25 | import org.axonframework.commandhandling.gateway.CommandGateway; 26 | import org.axonframework.messaging.responsetypes.ResponseTypes; 27 | import org.axonframework.queryhandling.QueryGateway; 28 | import org.springframework.http.HttpStatus; 29 | import org.springframework.web.bind.annotation.DeleteMapping; 30 | import org.springframework.web.bind.annotation.GetMapping; 31 | import org.springframework.web.bind.annotation.PatchMapping; 32 | import org.springframework.web.bind.annotation.PathVariable; 33 | import org.springframework.web.bind.annotation.PostMapping; 34 | import org.springframework.web.bind.annotation.RequestBody; 35 | import org.springframework.web.bind.annotation.RequestMapping; 36 | import org.springframework.web.bind.annotation.ResponseStatus; 37 | import org.springframework.web.bind.annotation.RestController; 38 | 39 | import java.util.List; 40 | import java.util.UUID; 41 | import java.util.concurrent.CompletableFuture; 42 | 43 | @RequiredArgsConstructor 44 | @RestController 45 | @RequestMapping("/api/restaurants") 46 | public class RestaurantController { 47 | 48 | private final CommandGateway commandGateway; 49 | private final QueryGateway queryGateway; 50 | 51 | @GetMapping 52 | public CompletableFuture> getRestaurants() { 53 | return queryGateway.query(new GetRestaurantsQuery(), ResponseTypes.multipleInstancesOf(Restaurant.class)) 54 | .thenApply(restaurants -> restaurants.stream().map(RestaurantResponse::from).toList()); 55 | } 56 | 57 | @GetMapping("/{id}") 58 | public CompletableFuture getRestaurant(@PathVariable UUID id) { 59 | return queryGateway.query(new GetRestaurantQuery(id.toString()), Restaurant.class) 60 | .thenApply(RestaurantResponse::from); 61 | } 62 | 63 | @ResponseStatus(HttpStatus.CREATED) 64 | @PostMapping 65 | public CompletableFuture addRestaurant(@Valid @RequestBody AddRestaurantRequest request) { 66 | return commandGateway.send(new AddRestaurantCommand(UUID.randomUUID().toString(), request.name())); 67 | } 68 | 69 | @PatchMapping("/{restaurantId}") 70 | public CompletableFuture updateRestaurant(@PathVariable UUID restaurantId, 71 | @Valid @RequestBody UpdateRestaurantRequest request) { 72 | return commandGateway.send(new UpdateRestaurantCommand(restaurantId.toString(), request.name())); 73 | } 74 | 75 | @DeleteMapping("/{restaurantId}") 76 | public CompletableFuture deleteRestaurant(@PathVariable UUID restaurantId) { 77 | return commandGateway.send(new DeleteRestaurantCommand(restaurantId.toString())); 78 | } 79 | 80 | @ResponseStatus(HttpStatus.CREATED) 81 | @PostMapping("/{restaurantId}/dishes") 82 | public CompletableFuture addRestaurantDish(@PathVariable UUID restaurantId, 83 | @Valid @RequestBody AddRestaurantDishRequest request) { 84 | return commandGateway.send(new AddRestaurantDishCommand(restaurantId.toString(), UUID.randomUUID().toString(), 85 | request.name(), request.price())); 86 | } 87 | 88 | @GetMapping("/{restaurantId}/dishes/{dishId}") 89 | public CompletableFuture getRestaurantDish(@PathVariable UUID restaurantId, @PathVariable UUID dishId) { 90 | return queryGateway.query(new GetRestaurantDishQuery(restaurantId.toString(), dishId.toString()), Dish.class) 91 | .thenApply(DishResponse::from); 92 | } 93 | 94 | @PatchMapping("/{restaurantId}/dishes/{dishId}") 95 | public CompletableFuture updateRestaurantDish(@PathVariable UUID restaurantId, @PathVariable UUID dishId, 96 | @Valid @RequestBody UpdateRestaurantDishRequest request) { 97 | return commandGateway.send(new UpdateRestaurantDishCommand(restaurantId.toString(), dishId.toString(), 98 | request.name(), request.price())); 99 | } 100 | 101 | @DeleteMapping("/{restaurantId}/dishes/{dishId}") 102 | public CompletableFuture deleteRestaurantDish(@PathVariable UUID restaurantId, @PathVariable UUID dishId) { 103 | return commandGateway.send(new DeleteRestaurantDishCommand(restaurantId.toString(), dishId.toString())); 104 | } 105 | 106 | @GetMapping("/{restaurantId}/orders") 107 | public CompletableFuture> getRestaurantOrders(@PathVariable UUID restaurantId) { 108 | return queryGateway.query(new GetRestaurantOrdersQuery(restaurantId.toString()), ResponseTypes.multipleInstancesOf(Order.class)) 109 | .thenApply(restaurants -> restaurants.stream().map(RestaurantOrderResponse::from).toList()); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/rest/dto/AddRestaurantDishRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.Positive; 6 | 7 | import java.math.BigDecimal; 8 | 9 | public record AddRestaurantDishRequest( 10 | @Schema(example = "Pizza Margherita 25cm") @NotBlank String name, 11 | @Schema(example = "6.99") @Positive BigDecimal price) { 12 | } 13 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/rest/dto/AddRestaurantRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | import jakarta.validation.constraints.NotBlank; 5 | 6 | public record AddRestaurantRequest(@Schema(example = "PizzaHut") @NotBlank String name) { 7 | } 8 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/rest/dto/DishResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.rest.dto; 2 | 3 | import com.ivanfranchin.restaurantservice.model.Dish; 4 | 5 | import java.math.BigDecimal; 6 | 7 | public record DishResponse(String restaurantId, String dishId, String dishName, BigDecimal dishPrice) { 8 | 9 | public static DishResponse from(Dish dish) { 10 | return new DishResponse( 11 | dish.getRestaurant().getId(), 12 | dish.getId(), 13 | dish.getName(), 14 | dish.getPrice()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/rest/dto/RestaurantOrderResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.rest.dto; 2 | 3 | import com.ivanfranchin.restaurantservice.model.Order; 4 | 5 | import java.math.BigDecimal; 6 | import java.time.ZonedDateTime; 7 | import java.util.Set; 8 | import java.util.stream.Collectors; 9 | 10 | public record RestaurantOrderResponse(String id, 11 | String customerName, 12 | String customerAddress, 13 | String status, 14 | BigDecimal total, 15 | ZonedDateTime createdAt, 16 | Set items) { 17 | 18 | public record OrderItem(String dishId, String dishName, BigDecimal dishPrice, Short quantity) { 19 | } 20 | 21 | public static RestaurantOrderResponse from(Order order) { 22 | Set items = order.getItems() 23 | .stream() 24 | .map(orderItem -> new OrderItem( 25 | orderItem.getDishId(), 26 | orderItem.getDishName(), 27 | orderItem.getDishPrice(), 28 | orderItem.getQuantity())) 29 | .collect(Collectors.toSet()); 30 | 31 | return new RestaurantOrderResponse( 32 | order.getId(), 33 | order.getCustomerName(), 34 | order.getCustomerAddress(), 35 | order.getStatus(), 36 | order.getTotal(), 37 | order.getCreatedAt(), 38 | items); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/rest/dto/RestaurantResponse.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.rest.dto; 2 | 3 | import com.ivanfranchin.restaurantservice.model.Restaurant; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | 9 | public record RestaurantResponse(String id, String name, Set dishes) { 10 | 11 | public record Dish(String id, String name, BigDecimal price) { 12 | } 13 | 14 | public static RestaurantResponse from(Restaurant restaurant) { 15 | Set dishes = restaurant.getDishes() 16 | .stream() 17 | .map(dish -> new Dish( 18 | dish.getId(), 19 | dish.getName(), 20 | dish.getPrice())) 21 | .collect(Collectors.toSet()); 22 | 23 | return new RestaurantResponse( 24 | restaurant.getId(), 25 | restaurant.getName(), 26 | dishes); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/rest/dto/UpdateRestaurantDishRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | import java.math.BigDecimal; 6 | 7 | public record UpdateRestaurantDishRequest( 8 | @Schema(example = "Pizza Margherita 35cm") String name, 9 | @Schema(example = "7.99") BigDecimal price) { 10 | } 11 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/rest/dto/UpdateRestaurantRequest.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.rest.dto; 2 | 3 | import io.swagger.v3.oas.annotations.media.Schema; 4 | 5 | public record UpdateRestaurantRequest(@Schema(example = "McDonald's") String name) { 6 | } 7 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/websocket/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.websocket; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void registerStompEndpoints(StompEndpointRegistry registry) { 15 | registry.addEndpoint("/websocket").withSockJS(); 16 | } 17 | 18 | @Override 19 | public void configureMessageBroker(MessageBrokerRegistry registry) { 20 | registry.setApplicationDestinationPrefixes("/app"); 21 | registry.enableSimpleBroker("/topic"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /restaurant-service/src/main/java/com/ivanfranchin/restaurantservice/websocket/WebSocketHandler.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice.websocket; 2 | 3 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantAddedEvent; 4 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDeletedEvent; 5 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishAddedEvent; 6 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishDeletedEvent; 7 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantDishUpdatedEvent; 8 | import com.ivanfranchin.axoneventcommons.restaurant.RestaurantUpdatedEvent; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.axonframework.eventhandling.EventHandler; 12 | import org.springframework.messaging.simp.SimpMessagingTemplate; 13 | import org.springframework.stereotype.Service; 14 | 15 | @Slf4j 16 | @RequiredArgsConstructor 17 | @Service 18 | public class WebSocketHandler { 19 | 20 | private final SimpMessagingTemplate simpMessagingTemplate; 21 | 22 | @EventHandler 23 | public void handle(RestaurantAddedEvent event) { 24 | log.info("<=[E] Received an event: {}", event); 25 | simpMessagingTemplate.convertAndSend("/topic/restaurant/added", event); 26 | } 27 | 28 | @EventHandler 29 | public void handle(RestaurantUpdatedEvent event) { 30 | log.info("<=[E] Received an event: {}", event); 31 | simpMessagingTemplate.convertAndSend("/topic/restaurant/updated", event); 32 | } 33 | 34 | @EventHandler 35 | public void handle(RestaurantDeletedEvent event) { 36 | log.info("<=[E] Received an event: {}", event); 37 | simpMessagingTemplate.convertAndSend("/topic/restaurant/deleted", event); 38 | } 39 | 40 | @EventHandler 41 | public void handle(RestaurantDishAddedEvent event) { 42 | log.info("<=[E] Received an event: {}", event); 43 | simpMessagingTemplate.convertAndSend("/topic/restaurant/dish/added", event); 44 | } 45 | 46 | @EventHandler 47 | public void handle(RestaurantDishUpdatedEvent event) { 48 | log.info("<=[E] Received an event: {}", event); 49 | simpMessagingTemplate.convertAndSend("/topic/restaurant/dish/updated", event); 50 | } 51 | 52 | @EventHandler 53 | public void handle(RestaurantDishDeletedEvent event) { 54 | log.info("<=[E] Received an event: {}", event); 55 | simpMessagingTemplate.convertAndSend("/topic/restaurant/dish/deleted", event); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /restaurant-service/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.application.name=restaurant-service 2 | 3 | spring.datasource.url=jdbc:postgresql://${POSTGRES_HOST:localhost}:${POSTGRES_PORT:5432}/restaurantdb 4 | spring.datasource.username=postgres 5 | spring.datasource.password=postgres 6 | 7 | spring.jpa.hibernate.ddl-auto=update 8 | 9 | spring.main.allow-circular-references=true 10 | 11 | axon.axonserver.servers=${AXON_SERVER_HOST:localhost}:${AXON_SERVER_PORT:8124} 12 | #axon.serializer.general=jackson 13 | 14 | management.endpoints.web.exposure.include=beans,env,health,info,metrics,mappings 15 | management.endpoint.health.show-details=always 16 | 17 | springdoc.show-actuator=true 18 | springdoc.swagger-ui.groups-order=DESC 19 | springdoc.swagger-ui.disable-swagger-default-url=true 20 | -------------------------------------------------------------------------------- /restaurant-service/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | _ _ _ 2 | _ __ ___ ___| |_ __ _ _ _ _ __ __ _ _ __ | |_ ___ ___ _ ____ _(_) ___ ___ 3 | | '__/ _ \/ __| __/ _` | | | | '__/ _` | '_ \| __|____/ __|/ _ \ '__\ \ / / |/ __/ _ \ 4 | | | | __/\__ \ || (_| | |_| | | | (_| | | | | ||_____\__ \ __/ | \ V /| | (_| __/ 5 | |_| \___||___/\__\__,_|\__,_|_| \__,_|_| |_|\__| |___/\___|_| \_/ |_|\___\___| 6 | :: Spring Boot :: ${spring-boot.formatted-version} 7 | -------------------------------------------------------------------------------- /restaurant-service/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Restaurant-Service 6 | 7 | 8 | 9 | 10 | 11 |
    12 | 20 |
    21 |
    22 |
    23 |
    24 |
    25 |
    26 |
    27 |
    28 | 29 | 30 |
    31 |
    32 | 33 | 34 |
    35 |
    36 | 37 | 38 |
    39 |
    40 | 62 |
    63 |
    64 |
    65 |
    66 |
    67 | 68 | 69 | 76 | 77 | 78 | 86 | 87 | 88 | 95 |
    96 |
    97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /restaurant-service/src/test/java/com/ivanfranchin/restaurantservice/RestaurantServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.ivanfranchin.restaurantservice; 2 | 3 | import org.junit.jupiter.api.Disabled; 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | 7 | @Disabled 8 | @SpringBootTest 9 | class RestaurantServiceApplicationTests { 10 | 11 | @Test 12 | void contextLoads() { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/my-functions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | TIMEOUT=120 4 | 5 | # -- wait_for_container_log -- 6 | # $1: docker container name 7 | # S2: spring value to wait to appear in container logs 8 | function wait_for_container_log() { 9 | local log_waiting="Waiting for string '$2' in the $1 logs ..." 10 | echo "${log_waiting} It will timeout in ${TIMEOUT}s" 11 | SECONDS=0 12 | 13 | while true ; do 14 | local log=$(docker logs $1 2>&1 | grep "$2") 15 | if [ -n "$log" ] ; then 16 | echo $log 17 | break 18 | fi 19 | 20 | if [ $SECONDS -ge $TIMEOUT ] ; then 21 | echo "${log_waiting} TIMEOUT" 22 | break; 23 | fi 24 | sleep 1 25 | done 26 | } -------------------------------------------------------------------------------- /start-apps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source scripts/my-functions.sh 4 | 5 | echo 6 | echo "Starting customer-service..." 7 | 8 | docker run -d --rm --name customer-service -p 9080:8080 \ 9 | -e MYSQL_HOST=mysql -e AXON_SERVER_HOST=axon-server \ 10 | --network axon-springboot-websocket_default \ 11 | --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/9080)" ] || exit 1' \ 12 | ivanfranchin/customer-service:1.0.0 13 | 14 | echo 15 | echo "Starting restaurant-service..." 16 | 17 | docker run -d --rm --name restaurant-service -p 9081:8080 \ 18 | -e POSTGRES_HOST=postgres -e AXON_SERVER_HOST=axon-server \ 19 | --network axon-springboot-websocket_default \ 20 | --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/9081)" ] || exit 1' \ 21 | ivanfranchin/restaurant-service:1.0.0 22 | 23 | echo 24 | echo "Starting food-ordering-service..." 25 | 26 | docker run -d --rm --name food-ordering-service -p 9082:8080 \ 27 | -e MONGODB_HOST=mongodb -e AXON_SERVER_HOST=axon-server \ 28 | --network axon-springboot-websocket_default \ 29 | --health-cmd='[ -z "$(echo "" > /dev/tcp/localhost/9082)" ] || exit 1' \ 30 | ivanfranchin/food-ordering-service:1.0.0 31 | 32 | echo 33 | wait_for_container_log "customer-service" "Started" 34 | 35 | echo 36 | wait_for_container_log "restaurant-service" "Started" 37 | 38 | echo 39 | wait_for_container_log "food-ordering-service" "Started" 40 | 41 | printf "\n" 42 | printf "%21s | %21s | %37s |\n" "Application" "URL" "Swagger" 43 | printf "%21s + %21s + %37s |\n" "---------------------" "---------------------" "-------------------------------------" 44 | printf "%21s | %21s | %37s |\n" "customer-service" "http://localhost:9080" "http://localhost:9080/swagger-ui.html" 45 | printf "%21s | %21s | %37s |\n" "restaurant-service" "http://localhost:9081" "http://localhost:9081/swagger-ui.html" 46 | printf "%21s | %21s | %37s |\n" "food-ordering-service" "http://localhost:9082" "http://localhost:9082/swagger-ui.html" 47 | printf "\n" 48 | -------------------------------------------------------------------------------- /stop-apps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker stop customer-service restaurant-service food-ordering-service --------------------------------------------------------------------------------