Compare commits
279 Commits
wip-vagran
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7dcafe5dea | ||
|
|
1a2378b14f | ||
|
|
e1e37359dd | ||
|
|
2ca2fb029e | ||
|
|
4c6417ae4e | ||
|
|
edcfd9e22f | ||
|
|
6c841000bb | ||
|
|
cc65f39088 | ||
|
|
24cc85790c | ||
|
|
d2077e21aa | ||
|
|
ab1ba8b274 | ||
|
|
083047cd81 | ||
|
|
d168a8be2b | ||
|
|
1fbd1be4b8 | ||
|
|
56cadd3139 | ||
|
|
d30adea37c | ||
|
|
c157ba5720 | ||
|
|
fd0a4355a7 | ||
|
|
f750d9c01a | ||
|
|
909ad5fea9 | ||
|
|
5bd3f36bd8 | ||
|
|
903a4b07a3 | ||
|
|
623554b550 | ||
|
|
f4a7dbe204 | ||
|
|
c8b0185591 | ||
|
|
106ee9ee54 | ||
|
|
1c0ae8a38c | ||
|
|
db05a80e01 | ||
|
|
e143d0f49b | ||
|
|
60a1986f56 | ||
|
|
e75b00b07a | ||
|
|
4185064d35 | ||
|
|
8b7146479a | ||
|
|
bb9e40e964 | ||
|
|
c2e4df440b | ||
|
|
823d571015 | ||
|
|
01bc1709c3 | ||
|
|
ebca25a2c8 | ||
|
|
68a1269088 | ||
|
|
4b74d2c1f1 | ||
|
|
7260376b17 | ||
|
|
4b78658dd8 | ||
|
|
04fe9045ea | ||
|
|
29cfdb0abf | ||
|
|
790c76c602 | ||
|
|
f740098f5c | ||
|
|
85e6c5788d | ||
|
|
e94e24d7d8 | ||
|
|
93a51f531b | ||
|
|
b2d135665c | ||
|
|
e01e98a508 | ||
|
|
e4a0d6773d | ||
|
|
727c771add | ||
|
|
abeb29bb2b | ||
|
|
0c573e9124 | ||
|
|
00f5b5a3ae | ||
|
|
2db8e09570 | ||
|
|
2c97630aa6 | ||
|
|
334df47112 | ||
|
|
ab4026a0e9 | ||
|
|
d5d6fb95d0 | ||
|
|
f4b35d04e9 | ||
|
|
50fdad2328 | ||
|
|
974f2b991a | ||
|
|
b06d607b6e | ||
|
|
a947cbf287 | ||
|
|
473743dbc9 | ||
|
|
0f21a057b0 | ||
|
|
5fd5b71f06 | ||
|
|
c88ba88063 | ||
|
|
fe925ac14a | ||
|
|
8312cb309b | ||
|
|
5c96941c05 | ||
|
|
93fc13e7a8 | ||
|
|
459e2915a3 | ||
|
|
fe20a3fe90 | ||
|
|
b0bfa7f29f | ||
|
|
61a115351e | ||
|
|
fe220a8499 | ||
|
|
f4ec434997 | ||
|
|
5f11423993 | ||
|
|
6c2700baba | ||
|
|
e551b3d77c | ||
|
|
c52b803c3a | ||
|
|
8f4ac83f4b | ||
|
|
72a4ab1ee4 | ||
|
|
91f6fde5cd | ||
|
|
20cc17c68c | ||
|
|
0b39ba658a | ||
|
|
df4204f9a2 | ||
|
|
258646a4d6 | ||
|
|
12cc48b906 | ||
|
|
859a01a4bc | ||
|
|
fd63640307 | ||
|
|
c916bc85eb | ||
|
|
f9d81802f7 | ||
|
|
4f1bb4aa52 | ||
|
|
7c47d590df | ||
|
|
886956348b | ||
|
|
8b5b54ed01 | ||
|
|
732581a3cb | ||
|
|
192db2bfa5 | ||
|
|
8ae6db7229 | ||
|
|
c03a5fed8d | ||
|
|
ee45163f2c | ||
|
|
e6bf638b4b | ||
|
|
d6f60101db | ||
|
|
e54577d656 | ||
|
|
bae00f6bd7 | ||
|
|
68dca23a6b | ||
|
|
19c9f88a7f | ||
|
|
9b6956b8df | ||
|
|
e2de325df2 | ||
|
|
c9fa916cdd | ||
|
|
f79ebb4d18 | ||
|
|
8f2fc83a34 | ||
|
|
05f2f309e7 | ||
|
|
6cf774da2a | ||
|
|
3c2e9d374c | ||
|
|
55cb34ef4f | ||
|
|
dde554e442 | ||
|
|
fd75779093 | ||
|
|
c8291bec71 | ||
|
|
1005c47d83 | ||
|
|
47e9053285 | ||
|
|
f76912a6cf | ||
|
|
625ea6007e | ||
|
|
6fae59fdeb | ||
|
|
f846a32d95 | ||
|
|
2a712017f1 | ||
|
|
85613936f4 | ||
|
|
6f480ad11a | ||
|
|
2b0c405378 | ||
|
|
a5d1e7312c | ||
|
|
29d42fda9a | ||
|
|
1b53bd9147 | ||
|
|
d1328e4ce8 | ||
|
|
45bda8e14d | ||
|
|
d9e13ff669 | ||
|
|
e14787bce8 | ||
|
|
1d14ece9cf | ||
|
|
1e13d482a8 | ||
|
|
2467099c3e | ||
|
|
f4ec33d275 | ||
|
|
a91ade08b1 | ||
|
|
fb97767a06 | ||
|
|
1e7234166a | ||
|
|
ef444bde9b | ||
|
|
4b73b9beed | ||
|
|
d0bdd51406 | ||
|
|
b0b32d0a3f | ||
|
|
e9517fe30f | ||
|
|
f4e070e7bd | ||
|
|
5f8475d675 | ||
|
|
12e46582cd | ||
|
|
b0855ebd81 | ||
|
|
39309f23a1 | ||
|
|
bd3de1a938 | ||
|
|
f570ccbe90 | ||
|
|
fe0ce037de | ||
|
|
1e40b2a591 | ||
|
|
f1d97ac49c | ||
|
|
b2b68ce163 | ||
|
|
e486102018 | ||
|
|
e488df3d06 | ||
|
|
4f11433390 | ||
|
|
b573027fc2 | ||
|
|
3117f12402 | ||
|
|
5b029d8307 | ||
|
|
6d8376cfd5 | ||
|
|
f59f4c78dd | ||
|
|
4b3fe001e7 | ||
|
|
76f3c830af | ||
|
|
62d41e9d5b | ||
|
|
7748217973 | ||
|
|
9a9511b2c4 | ||
|
|
28eefb81ab | ||
|
|
70b552a961 | ||
|
|
2c5b5b4132 | ||
|
|
e3c0112e8f | ||
|
|
1a5442a060 | ||
|
|
0b81ae8c08 | ||
|
|
b6b1fb7f0f | ||
|
|
e3dddcbc7b | ||
|
|
e699220162 | ||
|
|
fe89adba09 | ||
|
|
a369c4989f | ||
|
|
dd0bb4551b | ||
|
|
888544b700 | ||
|
|
bf01ad8e00 | ||
|
|
faa4027305 | ||
|
|
d55ff55e96 | ||
|
|
bdb4d3db26 | ||
|
|
9276b1bdc8 | ||
|
|
23fba9e462 | ||
|
|
05ebfd7d14 | ||
|
|
7b54b9042d | ||
|
|
7c6328aa5e | ||
|
|
9967ad9c52 | ||
|
|
8f86f72d85 | ||
|
|
bdcdae862d | ||
|
|
bfeb2e2e16 | ||
|
|
b125b30304 | ||
|
|
febef227fd | ||
|
|
40e03dd8e6 | ||
|
|
63d0e21fc4 | ||
|
|
6409ed1ab9 | ||
|
|
710487ebf9 | ||
|
|
df0d391521 | ||
|
|
6d53f149c8 | ||
|
|
3a4ee4e90c | ||
|
|
5001127978 | ||
|
|
ba5077685e | ||
|
|
b16039ee53 | ||
|
|
c11f1b1a64 | ||
|
|
f4ecc093fe | ||
|
|
afa3cf0042 | ||
|
|
28216a082d | ||
|
|
5511d1318e | ||
|
|
c5778b1379 | ||
|
|
21d21b9833 | ||
|
|
4bc8e8408d | ||
|
|
91361a1b18 | ||
|
|
fb1069ebb9 | ||
|
|
e337f05c89 | ||
|
|
725814407d | ||
|
|
0a38ccd09e | ||
|
|
f887c9fefe | ||
|
|
587a9164b7 | ||
|
|
b07933121d | ||
|
|
98c01cceeb | ||
|
|
d1339729ec | ||
|
|
99d07667bb | ||
|
|
d36c01b331 | ||
|
|
c61cc82d0a | ||
|
|
65524286da | ||
|
|
99000f04ed | ||
|
|
7b2fcc8a7c | ||
|
|
8615ac1d1e | ||
|
|
8d1faedef3 | ||
|
|
84debbda28 | ||
|
|
f74196e1f2 | ||
|
|
a73fc2477b | ||
|
|
f14fd73f63 | ||
|
|
064151fa61 | ||
|
|
43916e10aa | ||
|
|
ffeb521e3b | ||
|
|
1e2e564c29 | ||
|
|
655f86071c | ||
|
|
ff00467b47 | ||
|
|
28147a707d | ||
|
|
e8e147e3eb | ||
|
|
8228738710 | ||
|
|
8c342e40ce | ||
|
|
42db215414 | ||
|
|
23ffa96f47 | ||
|
|
822c0efb6d | ||
|
|
5108bc1b27 | ||
|
|
8564ec3fb2 | ||
|
|
a217f1b1e6 | ||
|
|
92526f2780 | ||
|
|
8ab13e580b | ||
|
|
a095076c0c | ||
|
|
fc2bd9970b | ||
|
|
a5994ad496 | ||
|
|
bfd04ef726 | ||
|
|
5c85418cc4 | ||
|
|
c0a9d6ed7d | ||
|
|
92d0940222 | ||
|
|
7f1f2af188 | ||
|
|
51e9d6c7fa | ||
|
|
cd35ac3d31 | ||
|
|
5d7e56578f | ||
|
|
59ecaa804a | ||
|
|
06f14a54bf | ||
|
|
afb7c9bc49 | ||
|
|
e0691a61a2 | ||
|
|
cbee50658e | ||
|
|
85f8826741 |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -25,6 +25,12 @@ browserapp/reports
|
||||
loginapp/reports
|
||||
local_developer_config
|
||||
|
||||
js-frontend/node_modules
|
||||
js-frontend/build
|
||||
js-frontend/dist
|
||||
js-frontend/dist-intermediate
|
||||
|
||||
|
||||
|
||||
/web/web.log
|
||||
*.log
|
||||
|
||||
114
README.md
114
README.md
@@ -1,7 +1,7 @@
|
||||
#Event-Sourcing+CQRS example application
|
||||
# Event-Sourcing+CQRS example application
|
||||
|
||||
This example application is the money transfer application described in my talk [Building and deploying microservices with event sourcing, CQRS and Docker](http://plainoldobjects.com/presentations/building-and-deploying-microservices-with-event-sourcing-cqrs-and-docker/).
|
||||
This talk describe a way of architecting highly scalable and available applications that is based on microservices, polyglot persistence,
|
||||
This talk describes a way of architecting highly scalable and available applications that is based on microservices, polyglot persistence,
|
||||
event sourcing (ES) and command query responsibility segregation (CQRS).
|
||||
Applications consist of loosely coupled components that communicate using events.
|
||||
These components can be deployed either as separate services or packaged as a monolithic application for simplified development and testing.
|
||||
@@ -35,99 +35,89 @@ The following diagram shows the architecture:
|
||||
|
||||

|
||||
|
||||
There are four logical services:
|
||||
There are the following services:
|
||||
|
||||
* Accounts (command-side) - REST API for creating accounts
|
||||
* Money transfers (command-side) - REST API for transferring money
|
||||
* Account view updater (query-side) - subscribes to events and updates a MongoDB View
|
||||
* Account view reader (query-side) - REST API for retrieving accounts
|
||||
* Customers Service - REST API for creating customers
|
||||
* Accounts Service - REST API for creating accounts
|
||||
* Transactions Service - REST API for transferring money
|
||||
* Customers View Service - subscribes to events and updates a MongoDB View, and provides an API for retrieving customers
|
||||
* Accounts View Service - subscribes to events and updates a MongoDB View, and provides an API for retrieving accounts
|
||||
|
||||
One of the neat things about the modular architecture is that there are two ways to deploy these four services:
|
||||
|
||||
* monolithic-service - all services are packaged as a single Spring Boot executable JAR
|
||||
* Microservices - three separate Spring Boot executable JARs
|
||||
* accounts-command-side-service - command-side accounts
|
||||
* transactions-command-side-service - command-side money transfers
|
||||
* accounts-query-side-service - Account View Updater and Account View Reader
|
||||
There is also an [API gateway](http://microservices.io/patterns/apigateway.html) service that acts as a Facade in front of the services.
|
||||
|
||||
# About the examples
|
||||
|
||||
There are currently the following versions of the example application:
|
||||
|
||||
* java-spring - a Java and Spring Boot example
|
||||
* scala-spring - a Scala and Spring Boot example
|
||||
* scala-spring - a Scala and Spring Boot example (NOTE: this version is lagging the Java Spring and hasn't been updated in a longtime.)
|
||||
|
||||
Other examples will be added shortly including a Scala/Play example.
|
||||
|
||||
For more information, please see the [wiki](../../wiki)
|
||||
|
||||
# About the Event Store
|
||||
# About the Eventuate Platform
|
||||
|
||||
The application uses one of two event stores:
|
||||
The application is built using [Eventuate](http://eventuate.io/), which is an application platform for writing transactional microservices.
|
||||
It provides a simple yet powerful event-driven programming model that is based on event sourcing and Command Query Responsibility Segregation (CQRS).
|
||||
Eventuate solves the distributed data management problems inherent in a microservice architecture.
|
||||
It consists of a scalable, distributed event store and client libraries for various languages and frameworks including Java, Scala, and the Spring framework.
|
||||
|
||||
* Embedded SQL-based event store, which is great for integration tests.
|
||||
It is also used when running the monolithic version of the application.
|
||||
* Event Store server - this is a full featured event store.
|
||||
See this [wiki page](../../wiki/AboutTheEventStoreServer) for more details.
|
||||
There are two versions of Eventuate:
|
||||
|
||||
# Building the application (and running the tests)
|
||||
* [Eventuate SaaS server](http://eventuate.io/usingeventuate.html) - this is a full featured event store that is hosted on AWS
|
||||
* [Eventuate Local](http://eventuate.io/usingeventuate.html) - an open-source event store that is built using MySQL and Kafka
|
||||
|
||||
Both versions of the application use Gradle.
|
||||
To build an application, execute this command in the application's top-level directory:
|
||||
There is also an embedded test event store, which is great for integration tests.
|
||||
|
||||
# Building and running the microservices
|
||||
|
||||
This is a Gradle project.
|
||||
However, you do not need to install Gradle since it will be downloaded automatically.
|
||||
You just need to have Java 8 installed.
|
||||
|
||||
The details of how to build and run the services depend slightly on whether you are using Eventuate SaaS or Eventuate Local.
|
||||
|
||||
## Building and running using Eventuate SaaS
|
||||
|
||||
First, must [sign up to get your credentials](https://signup.eventuate.io/) in order to get free access to the SaaS version.
|
||||
|
||||
Next, build the application
|
||||
|
||||
```
|
||||
cd java-spring
|
||||
./gradlew assemble
|
||||
```
|
||||
|
||||
Note: you do not need to install Gradle.
|
||||
It will be automatically downloaded by `./gradlew`.
|
||||
|
||||
This will build a Spring Boot jar in each of the `*-service` directories.
|
||||
|
||||
You can also run the tests using `gradle build`.
|
||||
However, you must set some environment variables.
|
||||
|
||||
First, you need to tell the query side code how to connect to MongoDB:
|
||||
Next, you can launch the services using [Docker Compose](https://docs.docker.com/compose/):
|
||||
|
||||
```
|
||||
export SPRING_DATA_MONGODB_URI=mongodb://192.168.59.103/yourdb
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
[Docker Compose](https://docs.docker.com/compose/) is a great way to run MongoDB.
|
||||
You can run the `docker-compose up -d mongodb` to run MongoDB.
|
||||
Finally, you can open the home page, which is served up by the API Gateway: `http://$DOCKER_HOST_IP:8080`
|
||||
|
||||
Second, some of the tests in accounts-command-side-service, transactions-command-side-service, accounts-query-side-service and e2e-test need you need to set some environment variables that tell them how to connect to the Event Store server.
|
||||
But don't worry.
|
||||
The build is configured to ignore failures for those projects.
|
||||
Note: `DOCKER_HOST_IP` is the IP address of the machine where Docker is running, e.g. the IP address of the VirtualBox VM.
|
||||
|
||||
# Running the application
|
||||
## Building and running using Eventuate Local
|
||||
|
||||
To run the application, you must to set the SPRING_DATA_MONGODB_URI environment variable, which tells the query services how to connect to MongoDB.
|
||||
|
||||
There are a couple of different ways of running the application.
|
||||
|
||||
## Running the monolithic application
|
||||
|
||||
One option is to run the self-contained monolithic application.
|
||||
It uses the embedded event store.
|
||||
|
||||
Simply use this command:
|
||||
First, build the application
|
||||
|
||||
```
|
||||
java -jar monolithic-service/build/libs/monolithic-service.jar
|
||||
cd java-spring
|
||||
./gradlew assemble -P eventuateDriver=local
|
||||
```
|
||||
|
||||
## Running the microservices
|
||||
Next, launch the services using [Docker Compose](https://docs.docker.com/compose/):
|
||||
|
||||
The other option is to run the services separately.
|
||||
However, in order to do this you need to [get credentials for the Event Store](../../wiki/AboutTheEventStoreServer).
|
||||
```
|
||||
export DOCKER_HOST_IP=...
|
||||
docker-compose -f docker-compose-eventuate-local.yml up -d
|
||||
```
|
||||
|
||||
One way to run the services is to use the scripts `run-all-services.sh`, which runs the services, and `kill-all-services.sh`, which kills the processes.
|
||||
Note: You need to set `DOCKER_HOST_IP` before running Docker Compose.
|
||||
This must be an IP address or resolvable hostname.
|
||||
It cannot be `localhost`.
|
||||
See this [guide to setting `DOCKER_HOST_IP`](http://eventuate.io/docs/usingdocker.html) for more information.
|
||||
|
||||
A much better way, however, is to use Docker Compose.
|
||||
Simply run the command `docker-compose up` to launch the services.
|
||||
This will create containers for MongoDB and each of the services.
|
||||
|
||||
You can now, for example, use the curl commands in `handy-curl-commands.sh` to interact with the server.
|
||||
|
||||
You can also use the Swagger UI exposed by each service `http://host:port/swagger-ui.html`.
|
||||
Finally, you can open the home page, which is served up by the API Gateway: `http://$DOCKER_HOST_IP:8080`
|
||||
|
||||
103
Vagrantfile
vendored
103
Vagrantfile
vendored
@@ -1,103 +0,0 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
|
||||
# All Vagrant configuration is done below. The "2" in Vagrant.configure
|
||||
# configures the configuration version (we support older styles for
|
||||
# backwards compatibility). Please don't change it unless you know what
|
||||
# you're doing.
|
||||
Vagrant.configure(2) do |config|
|
||||
# The most common configuration options are documented and commented below.
|
||||
# For a complete reference, please see the online documentation at
|
||||
# https://docs.vagrantup.com.
|
||||
|
||||
# Every Vagrant development environment requires a box. You can search for
|
||||
# boxes at https://atlas.hashicorp.com/search.
|
||||
config.vm.box = "ubuntu/trusty64"
|
||||
|
||||
config.vm.provider "virtualbox" do |v|
|
||||
v.memory = 2048
|
||||
v.cpus = 2
|
||||
end
|
||||
|
||||
# Disable automatic box update checking. If you disable this, then
|
||||
# boxes will only be checked for updates when the user runs
|
||||
# `vagrant box outdated`. This is not recommended.
|
||||
# config.vm.box_check_update = false
|
||||
|
||||
# Create a forwarded port mapping which allows access to a specific port
|
||||
# within the machine from a port on the host machine. In the example below,
|
||||
# accessing "localhost:8080" will access port 80 on the guest machine.
|
||||
# config.vm.network "forwarded_port", guest: 80, host: 8080
|
||||
|
||||
# Create a private network, which allows host-only access to the machine
|
||||
# using a specific IP.
|
||||
# config.vm.network "private_network", ip: "192.168.33.10"
|
||||
|
||||
# Create a public network, which generally matched to bridged network.
|
||||
# Bridged networks make the machine appear as another physical device on
|
||||
# your network.
|
||||
# config.vm.network "public_network"
|
||||
|
||||
# Share an additional folder to the guest VM. The first argument is
|
||||
# the path on the host to the actual folder. The second argument is
|
||||
# the path on the guest to mount the folder. And the optional third
|
||||
# argument is a set of non-required options.
|
||||
# config.vm.synced_folder "../data", "/vagrant_data"
|
||||
|
||||
# Provider-specific configuration so you can fine-tune various
|
||||
# backing providers for Vagrant. These expose provider-specific options.
|
||||
# Example for VirtualBox:
|
||||
#
|
||||
# config.vm.provider "virtualbox" do |vb|
|
||||
# # Display the VirtualBox GUI when booting the machine
|
||||
# vb.gui = true
|
||||
#
|
||||
# # Customize the amount of memory on the VM:
|
||||
# vb.memory = "1024"
|
||||
# end
|
||||
#
|
||||
# View the documentation for the provider you are using for more
|
||||
# information on available options.
|
||||
|
||||
# Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
|
||||
# such as FTP and Heroku are also available. See the documentation at
|
||||
# https://docs.vagrantup.com/v2/push/atlas.html for more information.
|
||||
# config.push.define "atlas" do |push|
|
||||
# push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
|
||||
# end
|
||||
|
||||
# Enable provisioning with a shell script. Additional provisioners such as
|
||||
# Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
|
||||
# documentation for more information about their specific syntax and use.
|
||||
|
||||
config.vm.provision "shell", inline: <<-SHELL
|
||||
#!/bin/sh
|
||||
|
||||
# https://github.com/pussinboots/vagrant-devel/blob/master/provision/packages/java8.sh
|
||||
|
||||
if which java >/dev/null; then
|
||||
echo "skip java 8 installation"
|
||||
else
|
||||
echo "java 8 installation"
|
||||
apt-get install --yes python-software-properties
|
||||
add-apt-repository ppa:webupd8team/java
|
||||
apt-get update -qq
|
||||
echo debconf shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections
|
||||
echo debconf shared/accepted-oracle-license-v1-1 seen true | /usr/bin/debconf-set-selections
|
||||
apt-get install --yes oracle-java8-installer
|
||||
yes "" | apt-get -f install
|
||||
fi
|
||||
SHELL
|
||||
|
||||
config.vm.provision "docker" do |d|
|
||||
end
|
||||
|
||||
config.vm.provision "shell", inline: <<-SHELL
|
||||
if which docker-compose >/dev/null; then
|
||||
echo "skip docker-compose installation"
|
||||
else
|
||||
sudo bash -c "curl -L https://github.com/docker/compose/releases/download/1.6.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose ; chmod +x /usr/local/bin/docker-compose"
|
||||
fi
|
||||
SHELL
|
||||
|
||||
end
|
||||
@@ -2,10 +2,27 @@
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$DOCKER_HOST_IP" ] ; then
|
||||
if [ -z "$DOCKER_HOST" ] ; then
|
||||
export DOCKER_HOST_IP=`hostname`
|
||||
else
|
||||
echo using ${DOCKER_HOST?}
|
||||
XX=${DOCKER_HOST%\:*}
|
||||
export DOCKER_HOST_IP=${XX#tcp\:\/\/}
|
||||
fi
|
||||
echo set DOCKER_HOST_IP $DOCKER_HOST_IP
|
||||
fi
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
DOCKER_COMPOSE="docker-compose -p event-sourcing-examples"
|
||||
|
||||
if [ "$1" = "-f" ] ; then
|
||||
shift;
|
||||
DOCKER_COMPOSE="$DOCKER_COMPOSE -f ${1?}"
|
||||
shift
|
||||
fi
|
||||
|
||||
if [ "$1" = "--use-existing" ] ; then
|
||||
shift;
|
||||
else
|
||||
@@ -13,17 +30,16 @@ else
|
||||
${DOCKER_COMPOSE?} rm -v --force
|
||||
fi
|
||||
|
||||
${DOCKER_COMPOSE?} up -d mongodb
|
||||
NO_RM=false
|
||||
|
||||
if [ -z "$DOCKER_HOST_IP" ] ; then
|
||||
if which docker-machine >/dev/null; then
|
||||
export DOCKER_HOST_IP=$(docker-machine ip default)
|
||||
else
|
||||
export DOCKER_HOST_IP=localhost
|
||||
fi
|
||||
echo set DOCKER_HOST_IP $DOCKER_HOST_IP
|
||||
if [ "$1" = "--no-rm" ] ; then
|
||||
NO_RM=true
|
||||
shift
|
||||
fi
|
||||
|
||||
${DOCKER_COMPOSE?} up -d mongodb $EXTRA_INFRASTRUCTURE_SERVICES
|
||||
|
||||
|
||||
if [ -z "$SPRING_DATA_MONGODB_URI" ] ; then
|
||||
export SPRING_DATA_MONGODB_URI=mongodb://${DOCKER_HOST_IP?}/mydb
|
||||
echo Set SPRING_DATA_MONGODB_URI $SPRING_DATA_MONGODB_URI
|
||||
@@ -31,21 +47,24 @@ fi
|
||||
|
||||
export SERVICE_HOST=$DOCKER_HOST_IP
|
||||
|
||||
./gradlew $* build -x :e2e-test:test
|
||||
./gradlew $BUILD_AND_TEST_ALL_EXTRA_GRADLE_ARGS $* build -x :e2e-test:test
|
||||
|
||||
if [ -z "$EVENTUATE_API_KEY_ID" -o -z "$EVENTUATE_API_KEY_SECRET" ] ; then
|
||||
if [ -z "$EVENTUATE_LOCAL" ] && [ -z "$EVENTUATE_API_KEY_ID" -o -z "$EVENTUATE_API_KEY_SECRET" ] ; then
|
||||
echo You must set EVENTUATE_API_KEY_ID and EVENTUATE_API_KEY_SECRET
|
||||
exit -1
|
||||
fi
|
||||
|
||||
${DOCKER_COMPOSE?} build
|
||||
|
||||
${DOCKER_COMPOSE?} up -d
|
||||
|
||||
$DIR/wait-for-services.sh $DOCKER_HOST_IP 8080 8081 8082
|
||||
$DIR/wait-for-services.sh $DOCKER_HOST_IP 8080 8081 8082 8083 8084
|
||||
|
||||
set -e
|
||||
|
||||
./gradlew -a $* :e2e-test:cleanTest :e2e-test:test -P ignoreE2EFailures=false
|
||||
./gradlew $BUILD_AND_TEST_ALL_EXTRA_GRADLE_ARGS -a $* :e2e-test:cleanTest :e2e-test:test -P ignoreE2EFailures=false
|
||||
|
||||
${DOCKER_COMPOSE?} stop
|
||||
${DOCKER_COMPOSE?} rm -v --force
|
||||
if [ $NO_RM = false ] ; then
|
||||
${DOCKER_COMPOSE?} stop
|
||||
${DOCKER_COMPOSE?} rm -v --force
|
||||
fi
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
#! /bin/bash -e
|
||||
|
||||
for dir in java-spring scala-spring; do
|
||||
(cd $dir ; ./gradlew -b build.gradle $*)
|
||||
done
|
||||
|
||||
@@ -2,36 +2,10 @@ This is the Java/Spring version of the Event Sourcing/CQRS money transfer exampl
|
||||
|
||||
# About the application
|
||||
|
||||
This application consists of three microservices:
|
||||
This application consists of the following microservices:
|
||||
|
||||
* Account Service - the command side business logic for Accounts
|
||||
* Money Transfer Service - the command side business logic for Money Transfers
|
||||
* Query service - query side implementation of a MongoDB-based, denormalized view of Accounts and MoneyTransfers
|
||||
|
||||
The Account Service consists of the following modules:
|
||||
|
||||
* accounts-command-side-backend - the Account aggregate
|
||||
* accounts-command-side-web - a REST API for creating and retrieving Accounts
|
||||
* accounts-command-side-service - a standalone microservice
|
||||
|
||||
The Money Transfer Service consists of the following modules:
|
||||
|
||||
* transactions-command-side-backend - the MoneyTransfer aggregate
|
||||
* transactions-command-side-web - a REST API for creating and retrieving Money Transfers
|
||||
* transactions-command-side-service - a standalone microservice
|
||||
|
||||
The Query Service consists the following modules:
|
||||
|
||||
* accounts-query-side-backend - MongoDB-based, denormalized view of Accounts and MoneyTransfers
|
||||
* accounts-query-side-web - a REST API for querying the denormalized view
|
||||
* accounts-query-side-service - a standalone microservice
|
||||
|
||||
# Deploying the application
|
||||
|
||||
These services can be deployed either as either separate standalone services using the Event Store server, or they can be deployed as a monolithic application for simpified integration testing.
|
||||
|
||||
The three services can also be packaged as a single monolithic web application in order to be used with the embedded Event Store:
|
||||
|
||||
* monolithic-service - all-in-one, monolithic packaging of the application
|
||||
|
||||
|
||||
* Account View Service - query side implementation of a MongoDB-based, denormalized view of Accounts
|
||||
* Customer Service - the command side business logic for Customers
|
||||
* Customer View Service - query side implementation of a MongoDB-based, denormalized view of Customers
|
||||
* Transaction Service - the command side business logic for Money Transfers
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
apply plugin: 'java'
|
||||
|
||||
dependencies {
|
||||
|
||||
compile project(":common-backend")
|
||||
compile "net.chrisrichardson.eventstore.client:eventstore-java-client_2.10:$eventStoreClientVersion"
|
||||
|
||||
testCompile project(":testutil")
|
||||
testCompile "junit:junit:4.11"
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||
testCompile "net.chrisrichardson.eventstore.client:eventstore-jdbc_2.10:$eventStoreClientVersion"
|
||||
|
||||
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.Command;
|
||||
|
||||
interface AccountCommand extends Command {
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
|
||||
|
||||
|
||||
import net.chrisrichardson.eventstore.EntityWithIdAndVersion;
|
||||
import net.chrisrichardson.eventstore.repository.AggregateRepository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class AccountService {
|
||||
|
||||
private final AggregateRepository<Account, AccountCommand> accountRepository;
|
||||
|
||||
public AccountService(AggregateRepository<Account, AccountCommand> accountRepository) {
|
||||
this.accountRepository = accountRepository;
|
||||
}
|
||||
|
||||
public rx.Observable<EntityWithIdAndVersion<Account>> openAccount(BigDecimal initialBalance) {
|
||||
return accountRepository.save(new OpenAccountCommand(initialBalance));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.EntityIdentifier;
|
||||
import net.chrisrichardson.eventstore.javaapi.consumer.EventHandlerContext;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.DebitRecordedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.MoneyTransferCreatedEvent;
|
||||
import net.chrisrichardson.eventstore.subscriptions.*;
|
||||
import rx.Observable;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@EventSubscriber(id="accountEventHandlers")
|
||||
public class AccountWorkflow implements CompoundEventHandler {
|
||||
|
||||
@EventHandlerMethod
|
||||
public Observable<?> debitAccount(EventHandlerContext<MoneyTransferCreatedEvent> ctx) {
|
||||
MoneyTransferCreatedEvent event = ctx.getEvent();
|
||||
BigDecimal amount = event.getDetails().getAmount();
|
||||
EntityIdentifier transactionId = ctx.getEntityIdentifier();
|
||||
|
||||
EntityIdentifier fromAccountId = event.getDetails().getFromAccountId();
|
||||
|
||||
return ctx.update(Account.class, fromAccountId, new DebitAccountCommand(amount, transactionId));
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public Observable<?> creditAccount(EventHandlerContext<DebitRecordedEvent> ctx) {
|
||||
DebitRecordedEvent event = ctx.getEvent();
|
||||
BigDecimal amount = event.getDetails().getAmount();
|
||||
EntityIdentifier fromAccountId = event.getDetails().getToAccountId();
|
||||
EntityIdentifier transactionId = ctx.getEntityIdentifier();
|
||||
|
||||
return ctx.update(Account.class, fromAccountId, new CreditAccountCommand(amount, transactionId));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.Aggregate;
|
||||
import net.chrisrichardson.eventstore.EntityIdentifier;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class CreditAccountCommand implements AccountCommand {
|
||||
private final BigDecimal amount;
|
||||
private final EntityIdentifier transactionId;
|
||||
|
||||
public CreditAccountCommand(BigDecimal amount, EntityIdentifier transactionId) {
|
||||
|
||||
this.amount = amount;
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public EntityIdentifier getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.Aggregate;
|
||||
import net.chrisrichardson.eventstore.EntityIdentifier;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class DebitAccountCommand implements AccountCommand {
|
||||
private final BigDecimal amount;
|
||||
private final EntityIdentifier transactionId;
|
||||
|
||||
public DebitAccountCommand(BigDecimal amount, EntityIdentifier transactionId) {
|
||||
|
||||
this.amount = amount;
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public EntityIdentifier getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class OpenAccountCommand implements AccountCommand {
|
||||
|
||||
private BigDecimal initialBalance;
|
||||
|
||||
public OpenAccountCommand(BigDecimal initialBalance) {
|
||||
this.initialBalance = initialBalance;
|
||||
}
|
||||
|
||||
public BigDecimal getInitialBalance() {
|
||||
return initialBalance;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web.main;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.AccountsCommandSideServiceConfiguration;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
|
||||
public class AccountsCommandSideServiceMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AccountsCommandSideServiceConfiguration.class, args);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
|
||||
dependencies {
|
||||
compile project(":accounts-command-side-backend")
|
||||
compile project(":common-web")
|
||||
|
||||
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
|
||||
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||
testCompile "net.chrisrichardson.eventstore.client:eventstore-jdbc_2.10:$eventStoreClientVersion"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import rx.Observable;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/accounts")
|
||||
public class AccountController {
|
||||
|
||||
private AccountService accountService;
|
||||
|
||||
@Autowired
|
||||
public AccountController(AccountService accountService) {
|
||||
this.accountService = accountService;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
public Observable<CreateAccountResponse> createAccount(@Validated @RequestBody CreateAccountRequest request) {
|
||||
return accountService.openAccount(request.getInitialBalance())
|
||||
.map(entityAndEventInfo -> new CreateAccountResponse(entityAndEventInfo.getEntityIdentifier().getId()));
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.util.ObservableReturnValueHandler;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||
|
||||
@Configuration
|
||||
@Import({AccountConfiguration.class})
|
||||
@ComponentScan
|
||||
public class CommandSideWebAccountsConfiguration extends WebMvcConfigurerAdapter {
|
||||
|
||||
class FakeThing {}
|
||||
|
||||
@Bean
|
||||
public FakeThing init(RequestMappingHandlerAdapter adapter) {
|
||||
// https://jira.spring.io/browse/SPR-13083
|
||||
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<HandlerMethodReturnValueHandler>(adapter.getReturnValueHandlers());
|
||||
handlers.add(0, new ObservableReturnValueHandler());
|
||||
adapter.setReturnValueHandlers(handlers);
|
||||
return new FakeThing();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.DecimalMin;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class CreateAccountRequest {
|
||||
|
||||
@NotNull
|
||||
@DecimalMin("0")
|
||||
private BigDecimal initialBalance;
|
||||
|
||||
public CreateAccountRequest() {
|
||||
}
|
||||
|
||||
public CreateAccountRequest(BigDecimal initialBalance) {
|
||||
|
||||
this.initialBalance = initialBalance;
|
||||
}
|
||||
|
||||
public BigDecimal getInitialBalance() {
|
||||
return initialBalance;
|
||||
}
|
||||
|
||||
public void setInitialBalance(BigDecimal initialBalance) {
|
||||
this.initialBalance = initialBalance;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.jdbc.config.JdbcEventStoreConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@Configuration
|
||||
@Import({CommandSideWebAccountsConfiguration.class, JdbcEventStoreConfiguration.class})
|
||||
public class AccountControllerIntegrationTestConfiguration {
|
||||
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
apply plugin: 'java'
|
||||
|
||||
dependencies {
|
||||
compile project(":common-backend")
|
||||
|
||||
compile "net.chrisrichardson.eventstore.client:eventstore-java-client_2.10:$eventStoreClientVersion"
|
||||
compile "org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion"
|
||||
|
||||
compile 'com.fasterxml.jackson.core:jackson-core:2.4.3'
|
||||
compile 'com.fasterxml.jackson.core:jackson-databind:2.4.3'
|
||||
compile 'com.fasterxml.jackson.module:jackson-module-scala_2.10:2.4.3'
|
||||
|
||||
testCompile project(":testutil")
|
||||
testCompile "junit:junit:4.11"
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||
testCompile "net.chrisrichardson.eventstore.client:eventstore-jdbc_2.10:$eventStoreClientVersion"
|
||||
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
public class AccountChangeInfo {
|
||||
|
||||
private String changeId;
|
||||
private String transactionId;
|
||||
private String transactionType;
|
||||
private long amount;
|
||||
private long balanceDelta;
|
||||
|
||||
public AccountChangeInfo(String changeId, String transactionId, String transactionType, long amount, long balanceDelta) {
|
||||
this.changeId = changeId;
|
||||
this.transactionId = transactionId;
|
||||
this.transactionType = transactionType;
|
||||
this.amount = amount;
|
||||
this.balanceDelta = balanceDelta;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Created by cer on 11/21/14.
|
||||
*/
|
||||
public class AccountInfo {
|
||||
|
||||
private String id;
|
||||
private long balance;
|
||||
private List<AccountChangeInfo> changes;
|
||||
private List<AccountTransactionInfo> transactions;
|
||||
private String version;
|
||||
|
||||
private AccountInfo() {
|
||||
}
|
||||
|
||||
public AccountInfo(String id, long balance, List<AccountChangeInfo> changes, List<AccountTransactionInfo> transactions, String version) {
|
||||
|
||||
this.id = id;
|
||||
this.balance = balance;
|
||||
this.changes = changes;
|
||||
this.transactions = transactions;
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
||||
public List<AccountChangeInfo> getChanges() {
|
||||
return changes;
|
||||
}
|
||||
|
||||
public List<AccountTransactionInfo> getTransactions() {
|
||||
return transactions;
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
interface AccountInfoRepository extends MongoRepository<AccountInfo, String> {
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
import com.mongodb.WriteResult;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.data.mongodb.core.query.Update;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
|
||||
import static net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.MoneyUtil.toIntegerRepr;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.where;
|
||||
|
||||
public class AccountInfoUpdateService {
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private AccountInfoRepository accountInfoRepository;
|
||||
private MongoTemplate mongoTemplate;
|
||||
|
||||
public AccountInfoUpdateService(AccountInfoRepository accountInfoRepository, MongoTemplate mongoTemplate) {
|
||||
this.accountInfoRepository = accountInfoRepository;
|
||||
this.mongoTemplate = mongoTemplate;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void create(String accountId, BigDecimal initialBalance, String version) {
|
||||
try {
|
||||
accountInfoRepository.save(new AccountInfo(
|
||||
accountId,
|
||||
toIntegerRepr(initialBalance),
|
||||
Collections.<AccountChangeInfo>emptyList(),
|
||||
Collections.<AccountTransactionInfo>emptyList(),
|
||||
version));
|
||||
logger.info("Saved in mongo");
|
||||
} catch (Throwable t) {
|
||||
logger.error("Error during saving: ");
|
||||
logger.error("Error during saving: ", t);
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void addTransaction(String eventId, String fromAccountId, AccountTransactionInfo ti) {
|
||||
mongoTemplate.updateMulti(new Query(where("id").is(fromAccountId).and("version").lt(eventId)),
|
||||
new Update().
|
||||
push("transactions", ti).
|
||||
set("version", eventId),
|
||||
AccountInfo.class);
|
||||
}
|
||||
|
||||
|
||||
public void updateBalance(String accountId, String changeId, long balanceDelta, AccountChangeInfo ci) {
|
||||
WriteResult x = mongoTemplate.updateMulti(new Query(where("id").is(accountId).and("version").lt(changeId)),
|
||||
new Update().
|
||||
inc("balance", balanceDelta).
|
||||
push("changes", ci).
|
||||
set("version", changeId),
|
||||
AccountInfo.class);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.Aggregate;
|
||||
import net.chrisrichardson.eventstore.EntityIdentifier;
|
||||
import net.chrisrichardson.eventstore.EntityNotFoundException;
|
||||
import rx.Observable;
|
||||
|
||||
public class AccountQueryService {
|
||||
|
||||
private AccountInfoRepository accountInfoRepository;
|
||||
|
||||
public AccountQueryService(AccountInfoRepository accountInfoRepository) {
|
||||
this.accountInfoRepository = accountInfoRepository;
|
||||
}
|
||||
|
||||
public Observable<AccountInfo> findByAccountId(EntityIdentifier accountId) {
|
||||
AccountInfo account = accountInfoRepository.findOne(accountId.getId());
|
||||
if (account == null)
|
||||
return Observable.error(new AccountNotFoundException(accountId.getId()));
|
||||
else
|
||||
return Observable.just(account);
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
|
||||
import net.chrisrichardson.eventstore.EntityIdentifier;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountChangedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountCreditedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountDebitedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountOpenedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.MoneyTransferCreatedEvent;
|
||||
import net.chrisrichardson.eventstore.subscriptions.CompoundEventHandler;
|
||||
import net.chrisrichardson.eventstore.subscriptions.DispatchedEvent;
|
||||
import net.chrisrichardson.eventstore.subscriptions.EventHandlerMethod;
|
||||
import net.chrisrichardson.eventstore.subscriptions.EventSubscriber;
|
||||
import rx.Observable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.MoneyUtil.toIntegerRepr;
|
||||
|
||||
@EventSubscriber(id="querySideEventHandlers")
|
||||
public class AccountQueryWorkflow implements CompoundEventHandler {
|
||||
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private AccountInfoUpdateService accountInfoUpdateService;
|
||||
|
||||
public AccountQueryWorkflow(AccountInfoUpdateService accountInfoUpdateService) {
|
||||
this.accountInfoUpdateService = accountInfoUpdateService;
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public Observable<Object> create(DispatchedEvent<AccountOpenedEvent> de) {
|
||||
AccountOpenedEvent event = de.event();
|
||||
String id = de.getEntityIdentifier().getId();
|
||||
String eventId = de.eventId().asString();
|
||||
logger.info("**************** account version=" + id + ", " + eventId);
|
||||
BigDecimal initialBalance = event.getInitialBalance();
|
||||
accountInfoUpdateService.create(id, initialBalance, eventId);
|
||||
return Observable.just(null);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public Observable<Object> recordTransfer(DispatchedEvent<MoneyTransferCreatedEvent> de) {
|
||||
String eventId = de.eventId().asString();
|
||||
String moneyTransferId = de.getEntityIdentifier().getId();
|
||||
String fromAccountId = de.event().getDetails().getFromAccountId().getId();
|
||||
String toAccountId = de.event().getDetails().getToAccountId().getId();
|
||||
logger.info("**************** account version=" + fromAccountId + ", " + de.eventId().asString());
|
||||
logger.info("**************** account version=" + toAccountId + ", " + de.eventId().asString());
|
||||
|
||||
AccountTransactionInfo ti = new AccountTransactionInfo(moneyTransferId, fromAccountId, toAccountId, toIntegerRepr(de.event().getDetails().getAmount()));
|
||||
|
||||
|
||||
accountInfoUpdateService.addTransaction(eventId, fromAccountId, ti);
|
||||
accountInfoUpdateService.addTransaction(eventId, toAccountId, ti);
|
||||
|
||||
return Observable.just(null);
|
||||
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public Observable<Object> recordDebit(DispatchedEvent<AccountDebitedEvent> de) {
|
||||
return saveChange(de, -1);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public Observable<Object> recordCredit(DispatchedEvent<AccountCreditedEvent> de) {
|
||||
return saveChange(de, +1);
|
||||
}
|
||||
|
||||
public <T extends AccountChangedEvent> Observable<Object> saveChange(DispatchedEvent<T> de, int delta) {
|
||||
String changeId = de.eventId().asString();
|
||||
String transactionId = de.event().getTransactionId().getId();
|
||||
long amount = toIntegerRepr(de.event().getAmount());
|
||||
|
||||
long balanceDelta = amount * delta;
|
||||
AccountChangeInfo ci = new AccountChangeInfo(changeId, transactionId, de.event().getClass().getSimpleName(), amount, balanceDelta);
|
||||
String accountId = de.getEntityIdentifier().getId();
|
||||
logger.info("**************** account version=" + accountId + ", " + de.eventId().asString());
|
||||
|
||||
accountInfoUpdateService.updateBalance(accountId, changeId, balanceDelta, ci);
|
||||
|
||||
return Observable.just(null);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
public class AccountTransactionInfo {
|
||||
|
||||
private String transactionId;
|
||||
private String fromAccountId;
|
||||
private String toAccountId;
|
||||
private long amount;
|
||||
|
||||
public AccountTransactionInfo(String transactionId, String fromAccountId, String toAccountId, long amount) {
|
||||
this.transactionId = transactionId;
|
||||
this.fromAccountId = fromAccountId;
|
||||
this.toAccountId = toAccountId;
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web.main;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.AccountsQuerySideServiceConfiguration;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
|
||||
public class AccountsQuerySideServiceMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AccountsQuerySideServiceConfiguration.class, args);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
|
||||
dependencies {
|
||||
compile project(":accounts-query-side-backend")
|
||||
compile project(":common-web")
|
||||
|
||||
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
|
||||
compile "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web.queryside;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.QuerySideAccountConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.util.ObservableReturnValueHandler;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
|
||||
|
||||
@Configuration
|
||||
@Import({QuerySideAccountConfiguration.class})
|
||||
@ComponentScan
|
||||
public class QuerySideWebConfiguration extends WebMvcConfigurerAdapter {
|
||||
|
||||
class FakeThing {}
|
||||
|
||||
@Bean
|
||||
public FakeThing init(RequestMappingHandlerAdapter adapter) {
|
||||
// https://jira.spring.io/browse/SPR-13083
|
||||
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<HandlerMethodReturnValueHandler>(adapter.getReturnValueHandlers());
|
||||
handlers.add(0, new ObservableReturnValueHandler());
|
||||
adapter.setReturnValueHandlers(handlers);
|
||||
return new FakeThing();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web.queryside.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.EntityIdentifier;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountNotFoundException;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts.AccountQueryService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import rx.Observable;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@RestController
|
||||
public class AccountQueryController {
|
||||
|
||||
private AccountQueryService accountInfoQueryService;
|
||||
|
||||
@Autowired
|
||||
public AccountQueryController(AccountQueryService accountInfoQueryService) {
|
||||
this.accountInfoQueryService = accountInfoQueryService;
|
||||
}
|
||||
|
||||
@RequestMapping(value="/accounts/{accountId}", method = RequestMethod.GET)
|
||||
public Observable<GetAccountResponse> get(@PathVariable String accountId) {
|
||||
return accountInfoQueryService.findByAccountId(new EntityIdentifier(accountId))
|
||||
.map(accountInfo -> new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance())));
|
||||
}
|
||||
|
||||
@ResponseStatus(value= HttpStatus.NOT_FOUND, reason="account not found")
|
||||
@ExceptionHandler(AccountNotFoundException.class)
|
||||
public void accountNotFound() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
4
java-spring/accounts-service/Dockerfile
Normal file
4
java-spring/accounts-service/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM java:openjdk-8u91-jdk
|
||||
CMD java ${JAVA_OPTS} -jar accounts-service.jar
|
||||
EXPOSE 8080
|
||||
COPY build/libs/accounts-service.jar .
|
||||
@@ -1,20 +1,19 @@
|
||||
apply plugin: VerifyMongoDBConfigurationPlugin
|
||||
apply plugin: VerifyEventStoreEnvironmentPlugin
|
||||
apply plugin: EventuateDependencyPlugin
|
||||
|
||||
apply plugin: 'spring-boot'
|
||||
|
||||
dependencies {
|
||||
compile project(":accounts-query-side-web")
|
||||
compile project(":common-backend")
|
||||
compile project(":common-swagger")
|
||||
|
||||
compile "org.springframework.boot:spring-boot-starter-web"
|
||||
compile "org.springframework.boot:spring-boot-starter-actuator"
|
||||
|
||||
compile "net.chrisrichardson.eventstore.client:eventstore-http-stomp-client_2.10:$eventStoreClientVersion"
|
||||
|
||||
testCompile project(":testutil")
|
||||
testCompile "junit:junit:4.11"
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test"
|
||||
|
||||
testCompile "io.eventuate.client.java:eventuate-client-java-jdbc:$eventuateClientVersion"
|
||||
}
|
||||
|
||||
test {
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice;
|
||||
|
||||
import io.eventuate.javaclient.driver.EventuateDriverConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.web.AccountsWebConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@Configuration
|
||||
@Import({AccountsWebConfiguration.class, EventuateDriverConfiguration.class, CommonSwaggerConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan
|
||||
public class AccountsServiceMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AccountsServiceMain.class, args);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +1,31 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
|
||||
|
||||
import net.chrisrichardson.eventstore.Event;
|
||||
import net.chrisrichardson.eventstore.EventUtil;
|
||||
import net.chrisrichardson.eventstore.ReflectiveMutableCommandProcessingAggregate;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountCreditedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountDebitFailedDueToInsufficientFundsEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountDebitedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountOpenedEvent;
|
||||
import io.eventuate.Event;
|
||||
import io.eventuate.EventUtil;
|
||||
import io.eventuate.ReflectiveMutableCommandProcessingAggregate;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Account extends ReflectiveMutableCommandProcessingAggregate<Account, AccountCommand> {
|
||||
|
||||
private BigDecimal balance;
|
||||
private boolean deleted;
|
||||
|
||||
public List<Event> process(OpenAccountCommand cmd) {
|
||||
return EventUtil.events(new AccountOpenedEvent(cmd.getInitialBalance()));
|
||||
return EventUtil.events(new AccountOpenedEvent(cmd.getCustomerId(), cmd.getTitle(), cmd.getInitialBalance(), cmd.getDescription()));
|
||||
}
|
||||
|
||||
public List<Event> process(DeleteAccountCommand cmd) {
|
||||
return EventUtil.events(new AccountDeletedEvent());
|
||||
}
|
||||
|
||||
public List<Event> process(DebitAccountCommand cmd) {
|
||||
if(deleted)
|
||||
return new ArrayList<>();
|
||||
|
||||
if (balance.compareTo(cmd.getAmount()) < 0)
|
||||
return EventUtil.events(new AccountDebitFailedDueToInsufficientFundsEvent(cmd.getTransactionId()));
|
||||
else
|
||||
@@ -27,6 +33,9 @@ public class Account extends ReflectiveMutableCommandProcessingAggregate<Account
|
||||
}
|
||||
|
||||
public List<Event> process(CreditAccountCommand cmd) {
|
||||
if(deleted)
|
||||
return new ArrayList<>();
|
||||
|
||||
return EventUtil.events(new AccountCreditedEvent(cmd.getAmount(), cmd.getTransactionId()));
|
||||
}
|
||||
|
||||
@@ -34,6 +43,10 @@ public class Account extends ReflectiveMutableCommandProcessingAggregate<Account
|
||||
balance = event.getInitialBalance();
|
||||
}
|
||||
|
||||
public void apply(AccountDeletedEvent event) {
|
||||
deleted = true;
|
||||
}
|
||||
|
||||
public void apply(AccountDebitedEvent event) {
|
||||
balance = balance.subtract(event.getAmount());
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
|
||||
|
||||
|
||||
import io.eventuate.Command;
|
||||
|
||||
interface AccountCommand extends Command {
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
|
||||
|
||||
|
||||
import io.eventuate.AggregateRepository;
|
||||
import io.eventuate.EntityWithIdAndVersion;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class AccountService {
|
||||
|
||||
private final AggregateRepository<Account, AccountCommand> accountRepository;
|
||||
|
||||
public AccountService(AggregateRepository<Account, AccountCommand> accountRepository) {
|
||||
this.accountRepository = accountRepository;
|
||||
}
|
||||
|
||||
public CompletableFuture<EntityWithIdAndVersion<Account>> openAccount(String customerId, String title, BigDecimal initialBalance, String description) {
|
||||
return accountRepository.save(new OpenAccountCommand(customerId, title, initialBalance, description));
|
||||
}
|
||||
|
||||
public CompletableFuture<EntityWithIdAndVersion<Account>> deleteAccount(String accountId) {
|
||||
return accountRepository.update(accountId, new DeleteAccountCommand());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
|
||||
|
||||
|
||||
import io.eventuate.EntityWithIdAndVersion;
|
||||
import io.eventuate.EventHandlerContext;
|
||||
import io.eventuate.EventHandlerMethod;
|
||||
import io.eventuate.EventSubscriber;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.DebitRecordedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.MoneyTransferCreatedEvent;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@EventSubscriber(id = "accountEventHandlers")
|
||||
public class AccountWorkflow {
|
||||
|
||||
@EventHandlerMethod
|
||||
public CompletableFuture<?> debitAccount(EventHandlerContext<MoneyTransferCreatedEvent> ctx) {
|
||||
MoneyTransferCreatedEvent event = ctx.getEvent();
|
||||
BigDecimal amount = event.getDetails().getAmount();
|
||||
String transactionId = ctx.getEntityId();
|
||||
|
||||
String fromAccountId = event.getDetails().getFromAccountId();
|
||||
|
||||
return ctx.update(Account.class, fromAccountId, new DebitAccountCommand(amount, transactionId));
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public CompletableFuture<EntityWithIdAndVersion<Account>> creditAccount(EventHandlerContext<DebitRecordedEvent> ctx) {
|
||||
DebitRecordedEvent event = ctx.getEvent();
|
||||
BigDecimal amount = event.getDetails().getAmount();
|
||||
String fromAccountId = event.getDetails().getToAccountId();
|
||||
String transactionId = ctx.getEntityId();
|
||||
|
||||
return ctx.update(Account.class, fromAccountId, new CreditAccountCommand(amount, transactionId));
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
|
||||
|
||||
import net.chrisrichardson.eventstore.EventStore;
|
||||
import net.chrisrichardson.eventstore.javaapi.consumer.EnableJavaEventHandlers;
|
||||
import net.chrisrichardson.eventstore.repository.AggregateRepository;
|
||||
import io.eventuate.AggregateRepository;
|
||||
import io.eventuate.EventuateAggregateStore;
|
||||
import io.eventuate.javaclient.spring.EnableEventHandlers;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@EnableJavaEventHandlers
|
||||
public class AccountConfiguration {
|
||||
@EnableEventHandlers
|
||||
public class AccountsBackendConfiguration {
|
||||
|
||||
@Bean
|
||||
public AccountWorkflow accountWorkflow() {
|
||||
@@ -22,7 +22,7 @@ public class AccountConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AggregateRepository<Account, AccountCommand> accountRepository(EventStore eventStore) {
|
||||
public AggregateRepository<Account, AccountCommand> accountRepository(EventuateAggregateStore eventStore) {
|
||||
return new AggregateRepository<Account, AccountCommand>(Account.class, eventStore);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class CreditAccountCommand implements AccountCommand {
|
||||
private final BigDecimal amount;
|
||||
private final String transactionId;
|
||||
|
||||
public CreditAccountCommand(BigDecimal amount, String transactionId) {
|
||||
|
||||
this.amount = amount;
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public String getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class DebitAccountCommand implements AccountCommand {
|
||||
private final BigDecimal amount;
|
||||
private final String transactionId;
|
||||
|
||||
public DebitAccountCommand(BigDecimal amount, String transactionId) {
|
||||
|
||||
this.amount = amount;
|
||||
this.transactionId = transactionId;
|
||||
}
|
||||
|
||||
public BigDecimal getAmount() {
|
||||
return amount;
|
||||
}
|
||||
|
||||
public String getTransactionId() {
|
||||
return transactionId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
|
||||
|
||||
public class DeleteAccountCommand implements AccountCommand {
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
|
||||
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public class OpenAccountCommand implements AccountCommand {
|
||||
|
||||
private String customerId;
|
||||
private String title;
|
||||
private BigDecimal initialBalance;
|
||||
private String description;
|
||||
|
||||
public OpenAccountCommand(String customerId, String title, BigDecimal initialBalance, String description) {
|
||||
this.customerId = customerId;
|
||||
this.title = title;
|
||||
this.initialBalance = initialBalance;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public BigDecimal getInitialBalance() {
|
||||
return initialBalance;
|
||||
}
|
||||
|
||||
public String getCustomerId() {
|
||||
return customerId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.web;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountRequest;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountResponse;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.DeleteAccountResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/accounts")
|
||||
public class AccountController {
|
||||
|
||||
private AccountService accountService;
|
||||
|
||||
@Autowired
|
||||
public AccountController(AccountService accountService) {
|
||||
this.accountService = accountService;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST)
|
||||
public CompletableFuture<CreateAccountResponse> createAccount(@Validated @RequestBody CreateAccountRequest request) {
|
||||
return accountService.openAccount(request.getCustomerId(), request.getTitle(), request.getInitialBalance(), request.getDescription())
|
||||
.thenApply(entityAndEventInfo -> new CreateAccountResponse(entityAndEventInfo.getEntityId()));
|
||||
}
|
||||
|
||||
@RequestMapping(value = "{accountId}", method = RequestMethod.DELETE)
|
||||
public CompletableFuture<DeleteAccountResponse> deleteAccount(@PathVariable String accountId) {
|
||||
return accountService.deleteAccount(accountId)
|
||||
.thenApply(entityAndEventInfo -> new DeleteAccountResponse(accountId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.web;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountsBackendConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@Configuration
|
||||
@Import({AccountsBackendConfiguration.class})
|
||||
@ComponentScan
|
||||
public class AccountsWebConfiguration {
|
||||
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts.CreateAccountRequest;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts.CreateAccountResponse;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountRequest;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.CreateAccountResponse;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -25,7 +25,7 @@ public class AccountsCommandSideServiceIntegrationTest {
|
||||
private int port;
|
||||
|
||||
private String baseUrl(String path) {
|
||||
return "http://localhost:" + port + "/" + path;
|
||||
return "http://localhost:" + port + "/api" + path;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
@@ -33,21 +33,20 @@ public class AccountsCommandSideServiceIntegrationTest {
|
||||
|
||||
|
||||
@Test
|
||||
public void shouldCreateAccountsAndTransferMoney() {
|
||||
public void shouldCreateAccounts() {
|
||||
BigDecimal initialFromAccountBalance = new BigDecimal(500);
|
||||
BigDecimal initialToAccountBalance = new BigDecimal(100);
|
||||
BigDecimal amountToTransfer = new BigDecimal(150);
|
||||
String customerId = "00000000-00000000";
|
||||
String title = "My Account";
|
||||
|
||||
final CreateAccountResponse fromAccount = restTemplate.postForEntity(baseUrl("/accounts"), new CreateAccountRequest(initialFromAccountBalance), CreateAccountResponse.class).getBody();
|
||||
final CreateAccountResponse fromAccount = restTemplate.postForEntity(baseUrl("/accounts"), new CreateAccountRequest(customerId, title, "", initialFromAccountBalance), CreateAccountResponse.class).getBody();
|
||||
final String fromAccountId = fromAccount.getAccountId();
|
||||
|
||||
CreateAccountResponse toAccount = restTemplate.postForEntity(baseUrl("/accounts"), new CreateAccountRequest(initialToAccountBalance), CreateAccountResponse.class).getBody();
|
||||
CreateAccountResponse toAccount = restTemplate.postForEntity(baseUrl("/accounts"), new CreateAccountRequest(customerId, title, "", initialToAccountBalance), CreateAccountResponse.class).getBody();
|
||||
String toAccountId = toAccount.getAccountId();
|
||||
|
||||
Assert.assertNotNull(fromAccountId);
|
||||
Assert.assertNotNull(toAccountId);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.web.AccountsWebConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.AuthConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -12,7 +15,8 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@Import(AccountsCommandSideServiceConfiguration.class)
|
||||
@Import({AccountsWebConfiguration.class, AuthConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
public class AccountsCommandSideServiceTestConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
|
||||
|
||||
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.AbstractEntityEventTest;
|
||||
@@ -1,7 +1,6 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend;
|
||||
|
||||
import net.chrisrichardson.eventstore.CommandProcessingAggregates;
|
||||
import net.chrisrichardson.eventstore.Event;
|
||||
import io.eventuate.Event;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountOpenedEvent;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -14,14 +13,16 @@ public class AccountTest {
|
||||
@Test
|
||||
public void testSomething() {
|
||||
Account account = new Account();
|
||||
String title = "My Account";
|
||||
String customerId = "00000000-00000000";
|
||||
BigDecimal initialBalance = new BigDecimal(512);
|
||||
List<Event> events = CommandProcessingAggregates.processToList(account, (AccountCommand)new OpenAccountCommand(initialBalance));
|
||||
|
||||
List<Event> events = account.process(new OpenAccountCommand(customerId, title, initialBalance, ""));
|
||||
|
||||
Assert.assertEquals(1, events.size());
|
||||
Assert.assertEquals(AccountOpenedEvent.class, events.get(0).getClass());
|
||||
|
||||
account.applyEvent(events.get(0));
|
||||
|
||||
Assert.assertEquals(initialBalance, account.getBalance());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web.commandside.accounts;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.web;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
@@ -35,16 +35,16 @@ public class AccountControllerIntegrationTest {
|
||||
|
||||
@Test
|
||||
public void shouldCreateAccount() throws Exception {
|
||||
mockMvc.perform(post("/accounts")
|
||||
mockMvc.perform(post("/api/accounts")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"initialBalance\" : 500}")
|
||||
.content("{\"customerId\" : \"00000000-00000000\", \"initialBalance\" : 500}")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldRejectBadRequest() throws Exception {
|
||||
mockMvc.perform(post("/accounts")
|
||||
mockMvc.perform(post("/api/accounts")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"initialBalanceXXX\" : 500}")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
@@ -0,0 +1,13 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.web;
|
||||
|
||||
import io.eventuate.javaclient.spring.jdbc.EmbeddedTestAggregateStoreConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@Configuration
|
||||
@Import({AccountsWebConfiguration.class, EmbeddedTestAggregateStoreConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
public class AccountControllerIntegrationTestConfiguration {
|
||||
|
||||
}
|
||||
4
java-spring/accounts-view-service/Dockerfile
Normal file
4
java-spring/accounts-view-service/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM java:openjdk-8u91-jdk
|
||||
CMD java ${JAVA_OPTS} -jar accounts-view-service.jar
|
||||
EXPOSE 8080
|
||||
COPY build/libs/accounts-view-service.jar .
|
||||
24
java-spring/accounts-view-service/build.gradle
Normal file
24
java-spring/accounts-view-service/build.gradle
Normal file
@@ -0,0 +1,24 @@
|
||||
apply plugin: VerifyMongoDBConfigurationPlugin
|
||||
apply plugin: VerifyEventStoreEnvironmentPlugin
|
||||
apply plugin: EventuateDependencyPlugin
|
||||
|
||||
apply plugin: 'spring-boot'
|
||||
|
||||
dependencies {
|
||||
compile project(":common-swagger")
|
||||
compile project(":common-backend")
|
||||
|
||||
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
|
||||
compile "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||
compile "io.eventuate.client.java:eventuate-client-java-spring:$eventuateClientVersion"
|
||||
compile "org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion"
|
||||
|
||||
testCompile project(":testutil")
|
||||
testCompile "junit:junit:4.11"
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||
testCompile "io.eventuate.client.java:eventuate-client-java-jdbc:$eventuateClientVersion"
|
||||
}
|
||||
|
||||
test {
|
||||
ignoreFailures System.getenv("EVENTUATE_API_KEY_ID") == null
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice;
|
||||
|
||||
import io.eventuate.javaclient.driver.EventuateDriverConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.web.AccountViewWebConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@Configuration
|
||||
@Import({AccountViewWebConfiguration.class, EventuateDriverConfiguration.class, CommonSwaggerConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan
|
||||
public class AccountsViewServiceMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AccountsViewServiceMain.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Created by cer on 11/21/14.
|
||||
*/
|
||||
public class AccountInfo {
|
||||
|
||||
private String id;
|
||||
private String customerId;
|
||||
private String title;
|
||||
private String description;
|
||||
private long balance;
|
||||
private List<AccountChangeInfo> changes;
|
||||
private Map<String, AccountTransactionInfo> transactions;
|
||||
private String version;
|
||||
@JsonProperty("date")
|
||||
private Date creationDate;
|
||||
|
||||
private AccountInfo() {
|
||||
}
|
||||
|
||||
public AccountInfo(String id, String customerId, String title, String description, long balance, List<AccountChangeInfo> changes, Map<String, AccountTransactionInfo> transactions, String version) {
|
||||
this(id, customerId, title, description, balance, changes, transactions, version, new Date());
|
||||
}
|
||||
|
||||
public AccountInfo(String id, String customerId, String title, String description, long balance, List<AccountChangeInfo> changes, Map<String, AccountTransactionInfo> transactions, String version, Date creationDate) {
|
||||
|
||||
this.id = id;
|
||||
this.customerId = customerId;
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.balance = balance;
|
||||
this.changes = changes;
|
||||
this.transactions = transactions;
|
||||
this.version = version;
|
||||
this.creationDate = creationDate;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getCustomerId() {
|
||||
return customerId;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public long getBalance() {
|
||||
return balance;
|
||||
}
|
||||
|
||||
public List<AccountChangeInfo> getChanges() {
|
||||
return changes == null ? Collections.EMPTY_LIST : changes;
|
||||
}
|
||||
|
||||
public List<AccountTransactionInfo> getTransactions() {
|
||||
return transactions == null ? Collections.EMPTY_LIST : new ArrayList<>(transactions.values());
|
||||
}
|
||||
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public Date getCreationDate() {
|
||||
return creationDate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
|
||||
|
||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
interface AccountInfoRepository extends MongoRepository<AccountInfo, String> {
|
||||
|
||||
List<AccountInfo> findByCustomerId(String customerId);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
|
||||
|
||||
import com.mongodb.WriteResult;
|
||||
import io.eventuate.Int128;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.data.mongodb.core.query.Update;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
import static net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.MoneyUtil.toIntegerRepr;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.where;
|
||||
|
||||
public class AccountInfoUpdateService {
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private AccountInfoRepository accountInfoRepository;
|
||||
private MongoTemplate mongoTemplate;
|
||||
|
||||
public AccountInfoUpdateService(AccountInfoRepository accountInfoRepository, MongoTemplate mongoTemplate) {
|
||||
this.accountInfoRepository = accountInfoRepository;
|
||||
this.mongoTemplate = mongoTemplate;
|
||||
}
|
||||
|
||||
|
||||
public void create(String accountId, String customerId, String title, BigDecimal initialBalance, String description, Int128 version) {
|
||||
try {
|
||||
AccountChangeInfo ci = new AccountChangeInfo();
|
||||
ci.setAmount(toIntegerRepr(initialBalance));
|
||||
WriteResult x = mongoTemplate.upsert(new Query(where("id").is(accountId).and("version").exists(false)),
|
||||
new Update()
|
||||
.set("customerId", customerId)
|
||||
.set("title", title)
|
||||
.set("description", description)
|
||||
.set("balance", toIntegerRepr(initialBalance))
|
||||
.push("changes", ci)
|
||||
.set("creationDate", new Date(version.getHi()))
|
||||
.set("version", version.asString()),
|
||||
AccountInfo.class);
|
||||
logger.info("Saved in mongo");
|
||||
|
||||
} catch (DuplicateKeyException t) {
|
||||
logger.warn("When saving ", t);
|
||||
} catch (Throwable t) {
|
||||
logger.error("Error during saving: ", t);
|
||||
throw new RuntimeException(t);
|
||||
}
|
||||
}
|
||||
|
||||
public void delete(String accountId) {
|
||||
accountInfoRepository.delete(accountId);
|
||||
}
|
||||
|
||||
|
||||
public void addTransaction(String accountId, AccountTransactionInfo ti) {
|
||||
mongoTemplate.upsert(new Query(where("id").is(accountId)),
|
||||
new Update().
|
||||
set("transactions." + ti.getTransactionId(), ti),
|
||||
AccountInfo.class);
|
||||
}
|
||||
|
||||
|
||||
public void updateBalance(String accountId, String changeId, long balanceDelta, AccountChangeInfo ci) {
|
||||
WriteResult x = mongoTemplate.updateMulti(new Query(where("id").is(accountId).and("version").lt(changeId)),
|
||||
new Update().
|
||||
inc("balance", balanceDelta).
|
||||
push("changes", ci).
|
||||
set("version", changeId),
|
||||
AccountInfo.class);
|
||||
}
|
||||
|
||||
public void updateTransactionStatus(String accountId, String transactionId, TransferState status) {
|
||||
mongoTemplate.upsert(new Query(where("id").is(accountId)),
|
||||
new Update().
|
||||
set("transactions." + transactionId + ".status", status),
|
||||
AccountInfo.class);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
|
||||
|
||||
public class AccountNotFoundException extends RuntimeException {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class AccountQueryService {
|
||||
|
||||
private AccountInfoRepository accountInfoRepository;
|
||||
|
||||
public AccountQueryService(AccountInfoRepository accountInfoRepository) {
|
||||
this.accountInfoRepository = accountInfoRepository;
|
||||
}
|
||||
|
||||
public AccountInfo findByAccountId(String accountId) {
|
||||
AccountInfo account = accountInfoRepository.findOne(accountId);
|
||||
if (account == null)
|
||||
throw new AccountNotFoundException(accountId);
|
||||
else
|
||||
return account;
|
||||
}
|
||||
|
||||
public List<AccountInfo> findByCustomerId(String customerId) {
|
||||
return accountInfoRepository.findByCustomerId(customerId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
|
||||
|
||||
import io.eventuate.DispatchedEvent;
|
||||
import io.eventuate.EventHandlerMethod;
|
||||
import io.eventuate.EventSubscriber;
|
||||
import io.eventuate.Int128;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.*;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.CreditRecordedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.DebitRecordedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.FailedDebitRecordedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.MoneyTransferCreatedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import static net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.MoneyUtil.toIntegerRepr;
|
||||
|
||||
@EventSubscriber(id="querySideEventHandlers")
|
||||
public class AccountQueryWorkflow {
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
|
||||
private AccountInfoUpdateService accountInfoUpdateService;
|
||||
|
||||
public AccountQueryWorkflow(AccountInfoUpdateService accountInfoUpdateService) {
|
||||
this.accountInfoUpdateService = accountInfoUpdateService;
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public void create(DispatchedEvent<AccountOpenedEvent> de) {
|
||||
AccountOpenedEvent event = de.getEvent();
|
||||
String id = de.getEntityId();
|
||||
Int128 eventId = de.getEventId();
|
||||
logger.info("**************** account version={}, {}", id, eventId);
|
||||
BigDecimal initialBalance = event.getInitialBalance();
|
||||
|
||||
String customerId = event.getCustomerId();
|
||||
String title = event.getTitle();
|
||||
String description = event.getDescription();
|
||||
|
||||
accountInfoUpdateService.create(id, customerId, title, initialBalance, description, eventId);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public void delete(DispatchedEvent<AccountDeletedEvent> de) {
|
||||
String id = de.getEntityId();
|
||||
accountInfoUpdateService.delete(id);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public void recordTransfer(DispatchedEvent<MoneyTransferCreatedEvent> de) {
|
||||
String eventId = de.getEventId().asString();
|
||||
String moneyTransferId = de.getEntityId();
|
||||
String fromAccountId = de.getEvent().getDetails().getFromAccountId();
|
||||
String toAccountId = de.getEvent().getDetails().getToAccountId();
|
||||
logger.info("**************** account version={}, {}", fromAccountId, eventId);
|
||||
logger.info("**************** account version={}, {}", toAccountId, eventId);
|
||||
|
||||
AccountTransactionInfo ti = new AccountTransactionInfo(moneyTransferId,
|
||||
fromAccountId,
|
||||
toAccountId,
|
||||
toIntegerRepr(de.getEvent().getDetails().getAmount()),
|
||||
de.getEvent().getDetails().getDate(),
|
||||
de.getEvent().getDetails().getDescription());
|
||||
|
||||
accountInfoUpdateService.addTransaction(fromAccountId, ti);
|
||||
accountInfoUpdateService.addTransaction(toAccountId, ti);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public void recordDebit(DispatchedEvent<AccountDebitedEvent> de) {
|
||||
saveChange(de, -1);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public void recordCredit(DispatchedEvent<AccountCreditedEvent> de) {
|
||||
saveChange(de, +1);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public void updateDebitTransactionState(DispatchedEvent<DebitRecordedEvent> de) {
|
||||
String transactionId = de.getEntityId();
|
||||
String fromAccountId = de.getEvent().getDetails().getFromAccountId();
|
||||
String toAccountId = de.getEvent().getDetails().getToAccountId();
|
||||
|
||||
accountInfoUpdateService.updateTransactionStatus(fromAccountId, transactionId, TransferState.DEBITED);
|
||||
accountInfoUpdateService.updateTransactionStatus(toAccountId, transactionId, TransferState.DEBITED);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public void updateCreditTransactionState(DispatchedEvent<CreditRecordedEvent> de) {
|
||||
String transactionId = de.getEntityId();
|
||||
String fromAccountId = de.getEvent().getDetails().getFromAccountId();
|
||||
String toAccountId = de.getEvent().getDetails().getToAccountId();
|
||||
|
||||
accountInfoUpdateService.updateTransactionStatus(fromAccountId, transactionId, TransferState.COMPLETED);
|
||||
accountInfoUpdateService.updateTransactionStatus(toAccountId, transactionId, TransferState.COMPLETED);
|
||||
}
|
||||
|
||||
@EventHandlerMethod
|
||||
public void recordFailed(DispatchedEvent<FailedDebitRecordedEvent> de) {
|
||||
String transactionId = de.getEntityId();
|
||||
String fromAccountId = de.getEvent().getDetails().getFromAccountId();
|
||||
String toAccountId = de.getEvent().getDetails().getToAccountId();
|
||||
|
||||
accountInfoUpdateService.updateTransactionStatus(fromAccountId, transactionId, TransferState.FAILED_DUE_TO_INSUFFICIENT_FUNDS);
|
||||
accountInfoUpdateService.updateTransactionStatus(toAccountId, transactionId, TransferState.FAILED_DUE_TO_INSUFFICIENT_FUNDS);
|
||||
}
|
||||
|
||||
public <T extends AccountChangedEvent> void saveChange(DispatchedEvent<T> de, int delta) {
|
||||
String changeId = de.getEventId().asString();
|
||||
String transactionId = de.getEvent().getTransactionId();
|
||||
long amount = toIntegerRepr(de.getEvent().getAmount());
|
||||
|
||||
long balanceDelta = amount * delta;
|
||||
AccountChangeInfo ci = new AccountChangeInfo(changeId, transactionId, de.getEvent().getClass().getSimpleName(), amount, balanceDelta);
|
||||
String accountId = de.getEntityId();
|
||||
logger.info("**************** account version={}, {}", accountId, de.getEventId().asString());
|
||||
|
||||
accountInfoUpdateService.updateBalance(accountId, changeId, balanceDelta, ci);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
|
||||
|
||||
|
||||
import net.chrisrichardson.eventstore.javaapi.consumer.EnableJavaEventHandlers;
|
||||
import io.eventuate.javaclient.spring.EnableEventHandlers;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
@@ -9,8 +9,8 @@ import org.springframework.data.mongodb.repository.config.EnableMongoRepositorie
|
||||
|
||||
@Configuration
|
||||
@EnableMongoRepositories
|
||||
@EnableJavaEventHandlers
|
||||
public class QuerySideAccountConfiguration {
|
||||
@EnableEventHandlers
|
||||
public class AccountViewBackendConfiguration {
|
||||
|
||||
@Bean
|
||||
public AccountQueryWorkflow accountQueryWorkflow(AccountInfoUpdateService accountInfoUpdateService) {
|
||||
@@ -27,8 +27,6 @@ public class QuerySideAccountConfiguration {
|
||||
return new AccountQueryService(accountInfoRepository);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public QuerySideDependencyChecker querysideDependencyChecker(MongoTemplate mongoTemplate) {
|
||||
return new QuerySideDependencyChecker(mongoTemplate);
|
||||
@@ -1,4 +1,4 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import rx.Observable;
|
||||
import rx.Subscriber;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class QuerySideDependencyChecker {
|
||||
private Logger logger = LoggerFactory.getLogger(getClass());
|
||||
@@ -22,17 +19,7 @@ public class QuerySideDependencyChecker {
|
||||
try {
|
||||
logger.info("Checking mongodb connectivity {}", System.getenv("SPRING_DATA_MONGODB_URI"));
|
||||
|
||||
Observable.<Object>create(new Observable.OnSubscribe<Object>() {
|
||||
@Override
|
||||
public void call(Subscriber<? super Object> subscriber) {
|
||||
try {
|
||||
subscriber.onNext(mongoTemplate.getDb().getCollectionNames());
|
||||
subscriber.onCompleted();
|
||||
} catch (Throwable t) {
|
||||
subscriber.onError(t);
|
||||
}
|
||||
}
|
||||
}).timeout(5, TimeUnit.SECONDS).toBlocking().first();
|
||||
mongoTemplate.getDb().getCollectionNames();
|
||||
|
||||
} catch (Throwable e) {
|
||||
throw new RuntimeException("Error connecting to Mongo - have you set SPRING_DATA_MONGODB_URI or --spring.data.mongodb_uri?", e);
|
||||
@@ -0,0 +1,64 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.web;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountNotFoundException;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountQueryService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.*;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class AccountQueryController {
|
||||
|
||||
private AccountQueryService accountInfoQueryService;
|
||||
|
||||
@Autowired
|
||||
public AccountQueryController(AccountQueryService accountInfoQueryService) {
|
||||
this.accountInfoQueryService = accountInfoQueryService;
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/accounts/{accountId}", method = RequestMethod.GET)
|
||||
public ResponseEntity<GetAccountResponse> get(@PathVariable String accountId) {
|
||||
AccountInfo accountInfo = accountInfoQueryService.findByAccountId(accountId);
|
||||
return ResponseEntity.ok().body(new GetAccountResponse(accountInfo.getId(), new BigDecimal(accountInfo.getBalance()), accountInfo.getTitle(), accountInfo.getDescription()));
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/customers/{customerId}/accounts", method = RequestMethod.GET)
|
||||
public ResponseEntity<GetAccountsResponse> getAccountsForCustomer(@PathVariable("customerId") String customerId) {
|
||||
return ResponseEntity.ok().body(
|
||||
new GetAccountsResponse(
|
||||
accountInfoQueryService.findByCustomerId(customerId)
|
||||
.stream()
|
||||
.map(accountInfo -> new GetAccountResponse(
|
||||
accountInfo.getId(),
|
||||
new BigDecimal(accountInfo.getBalance()),
|
||||
accountInfo.getTitle(),
|
||||
accountInfo.getDescription()))
|
||||
.collect(Collectors.toList())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/accounts/{accountId}/history", method = RequestMethod.GET)
|
||||
public ResponseEntity<AccountHistoryResponse> getTransactionsHistory(@PathVariable String accountId) {
|
||||
AccountInfo accountInfo = accountInfoQueryService.findByAccountId(accountId);
|
||||
List<AccountHistoryEntry> historyEntries = new ArrayList<>();
|
||||
historyEntries.add(new AccountOpenInfo(accountInfo.getCreationDate(), AccountHistoryEntry.EntryType.account, accountInfo.getChanges().get(0).getAmount()));
|
||||
accountInfo.getTransactions().forEach(historyEntries::add);
|
||||
|
||||
return ResponseEntity.ok().body(new AccountHistoryResponse(historyEntries));
|
||||
}
|
||||
|
||||
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "account not found")
|
||||
@ExceptionHandler(AccountNotFoundException.class)
|
||||
public void accountNotFound() {
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,6 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web;
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.web;
|
||||
|
||||
import net.chrisrichardson.eventstore.client.config.EventStoreHttpClientConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.queryside.QuerySideWebConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountViewBackendConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
@@ -13,11 +10,9 @@ import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
|
||||
@Configuration
|
||||
@Import({QuerySideWebConfiguration.class, EventStoreHttpClientConfiguration.class, CommonSwaggerConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
@Import({AccountViewBackendConfiguration.class})
|
||||
@ComponentScan
|
||||
public class AccountsQuerySideServiceConfiguration {
|
||||
|
||||
public class AccountViewWebConfiguration {
|
||||
|
||||
@Bean
|
||||
public HttpMessageConverters customConverters() {
|
||||
@@ -0,0 +1,154 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web;
|
||||
|
||||
import io.eventuate.Int128;
|
||||
import io.eventuate.javaclient.spring.jdbc.EventuateJdbcEventStoreConfiguration;
|
||||
import io.eventuate.javaclient.spring.jdbc.IdGenerator;
|
||||
import io.eventuate.javaclient.spring.jdbc.IdGeneratorImpl;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountInfoUpdateService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountQueryService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountViewBackendConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.accounts.AccountCreditedEvent;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountChangeInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.AccountTransactionInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.IntegrationTest;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = AccountInfoUpdateServiceTest.AccountInfoUpdateServiceTestConfiguration.class)
|
||||
@IntegrationTest
|
||||
public class AccountInfoUpdateServiceTest {
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@Import({AccountViewBackendConfiguration.class, EventuateJdbcEventStoreConfiguration.class})
|
||||
public static class AccountInfoUpdateServiceTestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private AccountInfoUpdateService accountInfoUpdateService;
|
||||
|
||||
@Autowired
|
||||
private AccountQueryService accountQueryService;
|
||||
|
||||
@Test
|
||||
public void shouldSaveAccountInfo() throws ExecutionException, InterruptedException {
|
||||
IdGenerator x = new IdGeneratorImpl();
|
||||
String accountId = x.genId().asString();
|
||||
String customerId = x.genId().asString();
|
||||
Int128 version = x.genId();
|
||||
|
||||
String title = "Checking account";
|
||||
BigDecimal initialBalance = new BigDecimal("1345");
|
||||
String description = "Some account";
|
||||
|
||||
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
|
||||
|
||||
AccountInfo accountInfo = accountQueryService.findByAccountId(accountId);
|
||||
|
||||
assertEquals(accountId, accountInfo.getId());
|
||||
assertEquals(customerId, accountInfo.getCustomerId());
|
||||
assertEquals(title, accountInfo.getTitle());
|
||||
assertEquals(description, accountInfo.getDescription());
|
||||
assertEquals(initialBalance.longValue() * 100, accountInfo.getBalance());
|
||||
assertEquals(1, accountInfo.getChanges().size());
|
||||
assertTrue(accountInfo.getTransactions().isEmpty());
|
||||
assertEquals(version.asString(), accountInfo.getVersion());
|
||||
|
||||
|
||||
String changeId = x.genId().asString();
|
||||
|
||||
String transactionId = x.genId().asString();
|
||||
|
||||
AccountChangeInfo change = new AccountChangeInfo(changeId, transactionId, AccountCreditedEvent.class.getSimpleName(),
|
||||
500, +1);
|
||||
|
||||
accountInfoUpdateService.updateBalance(accountId, changeId, 500,
|
||||
change);
|
||||
|
||||
accountInfo = accountQueryService.findByAccountId(accountId);
|
||||
assertEquals(initialBalance.add(new BigDecimal(5)).longValue() * 100, accountInfo.getBalance());
|
||||
assertFalse(accountInfo.getChanges().isEmpty());
|
||||
|
||||
assertEquals(change, accountInfo.getChanges().get(1));
|
||||
|
||||
String eventId = x.genId().asString();
|
||||
|
||||
AccountTransactionInfo ti = new AccountTransactionInfo(transactionId, accountId, accountId, 5, new Date(), "A transfer");
|
||||
|
||||
accountInfoUpdateService.addTransaction(accountId, ti);
|
||||
|
||||
accountInfo = accountQueryService.findByAccountId(accountId);
|
||||
assertFalse(accountInfo.getTransactions().isEmpty());
|
||||
|
||||
assertEquals(ti, accountInfo.getTransactions().get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldHandleDuplicateSaveAccountInfo() throws ExecutionException, InterruptedException {
|
||||
IdGenerator x = new IdGeneratorImpl();
|
||||
String accountId = x.genId().asString();
|
||||
String customerId = x.genId().asString();
|
||||
Int128 version = x.genId();
|
||||
|
||||
String title = "Checking account";
|
||||
BigDecimal initialBalance = new BigDecimal("1345");
|
||||
String description = "Some account";
|
||||
|
||||
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
|
||||
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldUpdateTransactionStatus() {
|
||||
IdGenerator x = new IdGeneratorImpl();
|
||||
String accountId = x.genId().asString();
|
||||
String customerId = x.genId().asString();
|
||||
Int128 version = x.genId();
|
||||
|
||||
String title = "Checking account";
|
||||
BigDecimal initialBalance = new BigDecimal("1345");
|
||||
String description = "Some account";
|
||||
|
||||
accountInfoUpdateService.create(accountId, customerId, title, initialBalance, description, version);
|
||||
|
||||
String transactionId = x.genId().asString();
|
||||
|
||||
AccountTransactionInfo transactionInfo = new AccountTransactionInfo();
|
||||
transactionInfo.setTransactionId(transactionId);
|
||||
transactionInfo.setStatus(TransferState.INITIAL);
|
||||
|
||||
accountInfoUpdateService.addTransaction(accountId, transactionInfo);
|
||||
|
||||
AccountInfo accountInfo = accountQueryService.findByAccountId(accountId);
|
||||
assertEquals(accountId, accountInfo.getId());
|
||||
assertFalse(accountInfo.getTransactions().isEmpty());
|
||||
assertEquals(1, accountInfo.getTransactions().size());
|
||||
|
||||
assertEquals(TransferState.INITIAL, accountInfo.getTransactions().get(0).getStatus());
|
||||
|
||||
accountInfoUpdateService.updateTransactionStatus(accountId, transactionId, TransferState.COMPLETED);
|
||||
|
||||
accountInfo = accountQueryService.findByAccountId(accountId);
|
||||
assertEquals(accountId, accountInfo.getId());
|
||||
assertFalse(accountInfo.getTransactions().isEmpty());
|
||||
assertEquals(1, accountInfo.getTransactions().size());
|
||||
|
||||
assertEquals(TransferState.COMPLETED, accountInfo.getTransactions().get(0).getStatus());
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.queryside.accounts.GetAccountResponse;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.accounts.GetAccountResponse;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -13,10 +13,10 @@ import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Producer;
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Verifier;
|
||||
import rx.Observable;
|
||||
|
||||
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.eventually;
|
||||
|
||||
@@ -30,7 +30,7 @@ public class AccountsQuerySideServiceIntegrationTest {
|
||||
private int port;
|
||||
|
||||
private String baseUrl(String path) {
|
||||
return "http://localhost:" + port + "/" + path;
|
||||
return "http://localhost:" + port + "/api" + path;
|
||||
}
|
||||
|
||||
@Autowired
|
||||
@@ -51,8 +51,8 @@ public class AccountsQuerySideServiceIntegrationTest {
|
||||
eventually(
|
||||
new Producer<GetAccountResponse>() {
|
||||
@Override
|
||||
public Observable<GetAccountResponse> produce() {
|
||||
return Observable.just(restTemplate.getForEntity(baseUrl("/accounts/" + fromAccountId), GetAccountResponse.class).getBody());
|
||||
public CompletableFuture<GetAccountResponse> produce() {
|
||||
return CompletableFuture.completedFuture(restTemplate.getForEntity(baseUrl("/accounts/" + fromAccountId), GetAccountResponse.class).getBody());
|
||||
}
|
||||
},
|
||||
new Verifier<GetAccountResponse>() {
|
||||
@@ -1,7 +1,12 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.web;
|
||||
|
||||
import io.eventuate.javaclient.spring.jdbc.EmbeddedTestAggregateStoreConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.web.AccountViewWebConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonswagger.CommonSwaggerConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
@@ -12,7 +17,9 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@Import(AccountsQuerySideServiceConfiguration.class)
|
||||
@Import({AccountViewWebConfiguration.class, EmbeddedTestAggregateStoreConfiguration.class, CommonSwaggerConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan
|
||||
public class AccountsQuerySideServiceTestConfiguration {
|
||||
|
||||
@Bean
|
||||
4
java-spring/api-gateway-service/Dockerfile
Normal file
4
java-spring/api-gateway-service/Dockerfile
Normal file
@@ -0,0 +1,4 @@
|
||||
FROM java:openjdk-8u91-jdk
|
||||
CMD java ${JAVA_OPTS} -jar api-gateway-service.jar
|
||||
EXPOSE 8080
|
||||
COPY build/libs/api-gateway-service.jar .
|
||||
21
java-spring/api-gateway-service/build.gradle
Normal file
21
java-spring/api-gateway-service/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'spring-boot'
|
||||
|
||||
dependencies {
|
||||
compile project(":common-auth-web")
|
||||
|
||||
compile "org.apache.httpcomponents:httpclient:4.5"
|
||||
compile "org.apache.httpcomponents:fluent-hc:4.5.1"
|
||||
|
||||
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
|
||||
compile "org.springframework.boot:spring-boot-starter-actuator:$springBootVersion"
|
||||
|
||||
testCompile "junit:junit:4.11"
|
||||
}
|
||||
|
||||
task copyWebStatic(type: Copy) {
|
||||
from "../../js-frontend/build"
|
||||
into "build/resources/main/static"
|
||||
}
|
||||
|
||||
jar.dependsOn(copyWebStatic)
|
||||
@@ -0,0 +1,57 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.apigateway;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ConfigurationProperties(prefix = "api.gateway")
|
||||
public class ApiGatewayProperties {
|
||||
|
||||
private List<Endpoint> endpoints;
|
||||
|
||||
public static class Endpoint {
|
||||
private String path;
|
||||
private RequestMethod method;
|
||||
private String location;
|
||||
|
||||
public Endpoint() {
|
||||
}
|
||||
|
||||
public Endpoint(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public RequestMethod getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public void setMethod(RequestMethod method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
}
|
||||
|
||||
public List<Endpoint> getEndpoints() {
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
public void setEndpoints(List<Endpoint> endpoints) {
|
||||
this.endpoints = endpoints;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.apigateway;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.AuthConfiguration;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
@Configuration
|
||||
@ComponentScan
|
||||
@EnableAutoConfiguration
|
||||
@Import({AuthConfiguration.class})
|
||||
@EnableConfigurationProperties({ApiGatewayProperties.class})
|
||||
public class ApiGatewayServiceConfiguration extends WebMvcConfigurerAdapter {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate(HttpMessageConverters converters) {
|
||||
|
||||
// we have to define Apache HTTP client to use the PATCH verb
|
||||
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
|
||||
converter.setSupportedMediaTypes(MediaType.parseMediaTypes("application/json"));
|
||||
converter.setObjectMapper(new ObjectMapper());
|
||||
|
||||
HttpClient httpClient = HttpClients.createDefault();
|
||||
RestTemplate restTemplate = new RestTemplate(Collections.<HttpMessageConverter<?>>singletonList(converter));
|
||||
restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient));
|
||||
|
||||
restTemplate.setErrorHandler(new RestTemplateErrorHandler());
|
||||
|
||||
return restTemplate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.apigateway;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.web.client.ResponseErrorHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class RestTemplateErrorHandler implements ResponseErrorHandler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(RestTemplateErrorHandler.class);
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
log.error("Response error: {} {}", response.getStatusCode(), response.getStatusText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasError(ClientHttpResponse response) throws IOException {
|
||||
return RestUtil.isError(response.getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.apigateway;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public class RestUtil {
|
||||
|
||||
public static boolean isError(HttpStatus status) {
|
||||
HttpStatus.Series series = status.series();
|
||||
return (HttpStatus.Series.CLIENT_ERROR.equals(series)
|
||||
|| HttpStatus.Series.SERVER_ERROR.equals(series));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.apigateway.controller;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.ApiGatewayProperties;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.ContentRequestTransformer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.HeadersRequestTransformer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils.URLRequestTransformer;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpResponse;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.springframework.web.bind.annotation.RequestMethod.*;
|
||||
|
||||
@RestController
|
||||
public class GatewayController {
|
||||
|
||||
Logger logger = LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
@Autowired
|
||||
private ApiGatewayProperties apiGatewayProperties;
|
||||
|
||||
private HttpClient httpClient;
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
|
||||
|
||||
httpClient = HttpClients.custom()
|
||||
.setConnectionManager(cm)
|
||||
.build();
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/api/**", method = {GET, POST, DELETE})
|
||||
@ResponseBody
|
||||
public ResponseEntity<String> proxyRequest(HttpServletRequest request) throws NoSuchRequestHandlingMethodException, IOException, URISyntaxException {
|
||||
HttpUriRequest proxiedRequest = createHttpUriRequest(request);
|
||||
logger.info("request: {}", proxiedRequest);
|
||||
HttpResponse proxiedResponse = httpClient.execute(proxiedRequest);
|
||||
logger.info("Response {}", proxiedResponse.getStatusLine().getStatusCode());
|
||||
return new ResponseEntity<>(read(proxiedResponse.getEntity().getContent()), makeResponseHeaders(proxiedResponse), HttpStatus.valueOf(proxiedResponse.getStatusLine().getStatusCode()));
|
||||
}
|
||||
|
||||
private HttpHeaders makeResponseHeaders(HttpResponse response) {
|
||||
HttpHeaders result = new HttpHeaders();
|
||||
Header h = response.getFirstHeader("Content-Type");
|
||||
result.set(h.getName(), h.getValue());
|
||||
return result;
|
||||
}
|
||||
|
||||
private HttpUriRequest createHttpUriRequest(HttpServletRequest request) throws URISyntaxException, NoSuchRequestHandlingMethodException, IOException {
|
||||
URLRequestTransformer urlRequestTransformer = new URLRequestTransformer(apiGatewayProperties);
|
||||
ContentRequestTransformer contentRequestTransformer = new ContentRequestTransformer();
|
||||
HeadersRequestTransformer headersRequestTransformer = new HeadersRequestTransformer();
|
||||
headersRequestTransformer.setPredecessor(contentRequestTransformer);
|
||||
contentRequestTransformer.setPredecessor(urlRequestTransformer);
|
||||
|
||||
return headersRequestTransformer.transform(request).build();
|
||||
}
|
||||
|
||||
private String read(InputStream input) throws IOException {
|
||||
try (BufferedReader buffer = new BufferedReader(new InputStreamReader(input))) {
|
||||
return buffer.lines().collect(Collectors.joining("\n"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.apigateway.main;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.ApiGatewayServiceConfiguration;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
|
||||
/**
|
||||
* Created by Main on 19.01.2016.
|
||||
*/
|
||||
public class ApiGatewayServiceMain {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ApiGatewayServiceConfiguration.class, args);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils;
|
||||
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.entity.ContentType;
|
||||
import org.apache.http.entity.StringEntity;
|
||||
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ContentRequestTransformer extends ProxyRequestTransformer {
|
||||
|
||||
@Override
|
||||
public RequestBuilder transform(HttpServletRequest request) throws NoSuchRequestHandlingMethodException, URISyntaxException, IOException {
|
||||
RequestBuilder requestBuilder = predecessor.transform(request);
|
||||
|
||||
String requestContent = request.getReader().lines().collect(Collectors.joining(""));
|
||||
if (!requestContent.isEmpty()) {
|
||||
StringEntity entity = new StringEntity(requestContent, ContentType.APPLICATION_JSON);
|
||||
requestBuilder.setEntity(entity);
|
||||
}
|
||||
|
||||
return requestBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils;
|
||||
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Enumeration;
|
||||
|
||||
public class HeadersRequestTransformer extends ProxyRequestTransformer {
|
||||
|
||||
@Override
|
||||
public RequestBuilder transform(HttpServletRequest request) throws NoSuchRequestHandlingMethodException, URISyntaxException, IOException {
|
||||
RequestBuilder requestBuilder = predecessor.transform(request);
|
||||
|
||||
Enumeration<String> headerNames = request.getHeaderNames();
|
||||
while (headerNames.hasMoreElements()) {
|
||||
String headerName = headerNames.nextElement();
|
||||
String headerValue = request.getHeader(headerName);
|
||||
if (headerName.equals("x-access-token")) {
|
||||
requestBuilder.addHeader(headerName, headerValue);
|
||||
}
|
||||
}
|
||||
|
||||
return requestBuilder;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils;
|
||||
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public abstract class ProxyRequestTransformer {
|
||||
|
||||
protected ProxyRequestTransformer predecessor;
|
||||
|
||||
public abstract RequestBuilder transform(HttpServletRequest request) throws NoSuchRequestHandlingMethodException, URISyntaxException, IOException;
|
||||
|
||||
public void setPredecessor(ProxyRequestTransformer transformer) {
|
||||
this.predecessor = transformer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.apigateway.utils;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.apigateway.ApiGatewayProperties;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
|
||||
public class URLRequestTransformer extends ProxyRequestTransformer {
|
||||
|
||||
private ApiGatewayProperties apiGatewayProperties;
|
||||
|
||||
public URLRequestTransformer(ApiGatewayProperties apiGatewayProperties) {
|
||||
this.apiGatewayProperties = apiGatewayProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBuilder transform(HttpServletRequest request) throws NoSuchRequestHandlingMethodException, URISyntaxException {
|
||||
String requestURI = request.getRequestURI();
|
||||
URI uri;
|
||||
if (request.getQueryString() != null && !request.getQueryString().isEmpty()) {
|
||||
uri = new URI(getServiceUrl(requestURI, request) + "?" + request.getQueryString());
|
||||
} else {
|
||||
uri = new URI(getServiceUrl(requestURI, request));
|
||||
}
|
||||
|
||||
RequestBuilder rb = RequestBuilder.create(request.getMethod());
|
||||
rb.setUri(uri);
|
||||
return rb;
|
||||
}
|
||||
|
||||
private String getServiceUrl(String requestURI, HttpServletRequest httpServletRequest) throws NoSuchRequestHandlingMethodException {
|
||||
|
||||
ApiGatewayProperties.Endpoint endpoint =
|
||||
apiGatewayProperties.getEndpoints().stream()
|
||||
.filter(e ->
|
||||
requestURI.matches(e.getPath()) && e.getMethod() == RequestMethod.valueOf(httpServletRequest.getMethod())
|
||||
)
|
||||
.findFirst().orElseThrow(() -> new NoSuchRequestHandlingMethodException(httpServletRequest));
|
||||
return endpoint.getLocation() + requestURI;
|
||||
}
|
||||
}
|
||||
31
java-spring/api-gateway-service/src/main/resources/application.properties
Executable file
31
java-spring/api-gateway-service/src/main/resources/application.properties
Executable file
@@ -0,0 +1,31 @@
|
||||
accounts.commandside.service.host=localhost
|
||||
accounts.queryside.service.host=localhost
|
||||
customers.commandside.service.host=localhost
|
||||
customers.queryside.service.host=localhost
|
||||
transfers.commandside.service.host=localhost
|
||||
|
||||
|
||||
api.gateway.endpoints[0].path=[/]*api/accounts.*
|
||||
api.gateway.endpoints[0].method=GET
|
||||
api.gateway.endpoints[0].location=http://${accounts.queryside.service.host}:8080
|
||||
api.gateway.endpoints[1].path=[/]*api/customers.*/accounts
|
||||
api.gateway.endpoints[1].method=GET
|
||||
api.gateway.endpoints[1].location=http://${accounts.queryside.service.host}:8080
|
||||
api.gateway.endpoints[2].path=[/]*api/accounts.*
|
||||
api.gateway.endpoints[2].method=POST
|
||||
api.gateway.endpoints[2].location=http://${accounts.commandside.service.host}:8080
|
||||
api.gateway.endpoints[3].path=[/]*api/customers.*
|
||||
api.gateway.endpoints[3].method=GET
|
||||
api.gateway.endpoints[3].location=http://${customers.queryside.service.host}:8080
|
||||
api.gateway.endpoints[4].path=[/]*api/customers.*
|
||||
api.gateway.endpoints[4].method=POST
|
||||
api.gateway.endpoints[4].location=http://${customers.commandside.service.host}:8080
|
||||
api.gateway.endpoints[5].path=[/]*api/transfers.*
|
||||
api.gateway.endpoints[5].method=POST
|
||||
api.gateway.endpoints[5].location=http://${transfers.commandside.service.host}:8080
|
||||
api.gateway.endpoints[6].path=[/]*api/customers.*
|
||||
api.gateway.endpoints[6].method=DELETE
|
||||
api.gateway.endpoints[6].location=http://${customers.commandside.service.host}:8080
|
||||
api.gateway.endpoints[7].path=[/]*api/accounts.*
|
||||
api.gateway.endpoints[7].method=DELETE
|
||||
api.gateway.endpoints[7].location=http://${accounts.commandside.service.host}:8080
|
||||
@@ -12,8 +12,13 @@
|
||||
<root level="error">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
|
||||
<logger name="org.springframework.web" level='debug'>
|
||||
<logger name="org.springframework" level='info'>
|
||||
</logger>
|
||||
|
||||
</configuration>
|
||||
<logger name="net.chrisrichardson.eventstore.javaexamples.banking" level='info'>
|
||||
</logger>
|
||||
|
||||
<logger name="io.eventuate.activity" level='debug'>
|
||||
</logger>
|
||||
|
||||
</configuration>
|
||||
@@ -2,12 +2,14 @@ apply plugin: VerifyMongoDBConfigurationPlugin
|
||||
|
||||
dependencies {
|
||||
|
||||
testCompile project(":accounts-command-side-backend")
|
||||
testCompile project(":transactions-command-side-backend")
|
||||
testCompile project(":accounts-query-side-backend")
|
||||
testCompile project(":transactions-service")
|
||||
testCompile project(":accounts-service")
|
||||
testCompile project(":accounts-view-service")
|
||||
testCompile project(":customers-service")
|
||||
testCompile project(":customers-view-service")
|
||||
testCompile project(":testutil")
|
||||
testCompile "junit:junit:4.11"
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||
testCompile "net.chrisrichardson.eventstore.client:eventstore-jdbc_2.10:$eventStoreClientVersion"
|
||||
testCompile "io.eventuate.client.java:eventuate-client-java-jdbc:$eventuateClientVersion"
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferConfiguration;
|
||||
import net.chrisrichardson.eventstore.jdbc.config.JdbcEventStoreConfiguration;
|
||||
import net.chrisrichardson.utils.config.MetricRegistryConfiguration;
|
||||
import io.eventuate.javaclient.spring.jdbc.EmbeddedTestAggregateStoreConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountsBackendConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransferBackendConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@Configuration
|
||||
@Import({AccountConfiguration.class, MoneyTransferConfiguration.class, JdbcEventStoreConfiguration.class})
|
||||
@Import({AccountsBackendConfiguration.class, MoneyTransferBackendConfiguration.class, EmbeddedTestAggregateStoreConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
public class BankingTestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend;
|
||||
|
||||
import net.chrisrichardson.eventstore.EntityWithIdAndVersion;
|
||||
import net.chrisrichardson.eventstore.EntityWithMetadata;
|
||||
import net.chrisrichardson.eventstore.EventStore;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.Account;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferService;
|
||||
import io.eventuate.EntityWithIdAndVersion;
|
||||
import io.eventuate.EventuateAggregateStore;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.Account;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.TransferDetails;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferState;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransfer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransferService;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -16,18 +15,15 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.IntegrationTest;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import rx.Observable;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Producer;
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Verifier;
|
||||
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.await;
|
||||
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.eventually;
|
||||
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes=BankingTestConfiguration.class)
|
||||
@SpringApplicationConfiguration(classes = BankingTestConfiguration.class)
|
||||
@IntegrationTest
|
||||
public class MoneyTransferIntegrationTest {
|
||||
|
||||
@@ -38,117 +34,57 @@ public class MoneyTransferIntegrationTest {
|
||||
private MoneyTransferService moneyTransferService;
|
||||
|
||||
@Autowired
|
||||
private EventStore eventStore;
|
||||
|
||||
private EventuateAggregateStore eventStore;
|
||||
|
||||
|
||||
@Test
|
||||
public void shouldTransferMoney() {
|
||||
final EntityWithIdAndVersion<Account> fromAccount= await(accountService.openAccount(new BigDecimal(150)));
|
||||
final EntityWithIdAndVersion<Account> fromAccount = await(accountService.openAccount("00000000-00000000", "My Account", new BigDecimal(150), ""));
|
||||
|
||||
final EntityWithIdAndVersion<Account> toAccount = await(accountService.openAccount(new BigDecimal(300)));
|
||||
final EntityWithIdAndVersion<Account> toAccount = await(accountService.openAccount("00000000-00000000", "My Account", new BigDecimal(300), ""));
|
||||
|
||||
final EntityWithIdAndVersion<MoneyTransfer> transaction = await(
|
||||
moneyTransferService.transferMoney(new TransferDetails(fromAccount.getEntityIdentifier(),
|
||||
toAccount.getEntityIdentifier(),
|
||||
moneyTransferService.transferMoney(new TransferDetails(fromAccount.getEntityId(),
|
||||
toAccount.getEntityId(),
|
||||
new BigDecimal(80))));
|
||||
|
||||
|
||||
eventually (
|
||||
new Producer<EntityWithMetadata<Account>>() {
|
||||
@Override
|
||||
public Observable<EntityWithMetadata<Account>> produce() {
|
||||
return (Observable<EntityWithMetadata<Account>>)eventStore.find(Account.class, fromAccount.getEntityIdentifier());
|
||||
}
|
||||
},
|
||||
new Verifier<EntityWithMetadata<Account>>() {
|
||||
@Override
|
||||
public void verify(EntityWithMetadata<Account> account) {
|
||||
Assert.assertEquals(new BigDecimal(70), account.entity().getBalance());
|
||||
}
|
||||
});
|
||||
eventually(
|
||||
() -> eventStore.find(Account.class, fromAccount.getEntityId()),
|
||||
account -> Assert.assertEquals(new BigDecimal(70), account.getEntity().getBalance()));
|
||||
|
||||
eventually (
|
||||
new Producer<EntityWithMetadata<Account>>() {
|
||||
@Override
|
||||
public Observable<EntityWithMetadata<Account>> produce() {
|
||||
return (Observable<EntityWithMetadata<Account>>)eventStore.find(Account.class, toAccount.getEntityIdentifier());
|
||||
}
|
||||
},
|
||||
new Verifier<EntityWithMetadata<Account>>() {
|
||||
@Override
|
||||
public void verify(EntityWithMetadata<Account> account) {
|
||||
Assert.assertEquals(new BigDecimal(380), account.entity().getBalance());
|
||||
}
|
||||
});
|
||||
eventually(
|
||||
() -> eventStore.find(Account.class, toAccount.getEntityId()),
|
||||
account -> Assert.assertEquals(new BigDecimal(380), account.getEntity().getBalance()));
|
||||
|
||||
eventually (
|
||||
new Producer<EntityWithMetadata<MoneyTransfer>>() {
|
||||
@Override
|
||||
public Observable<EntityWithMetadata<MoneyTransfer>> produce() {
|
||||
return (Observable<EntityWithMetadata<MoneyTransfer>>)eventStore.find(MoneyTransfer.class, transaction.getEntityIdentifier());
|
||||
}
|
||||
},
|
||||
new Verifier<EntityWithMetadata<MoneyTransfer>>() {
|
||||
@Override
|
||||
public void verify(EntityWithMetadata<MoneyTransfer> updatedTransaction) {
|
||||
Assert.assertEquals(TransferState.COMPLETED, updatedTransaction.entity().getState());
|
||||
}
|
||||
});
|
||||
eventually(
|
||||
() -> eventStore.find(MoneyTransfer.class, transaction.getEntityId()),
|
||||
updatedTransaction -> Assert.assertEquals(TransferState.COMPLETED, updatedTransaction.getEntity().getState()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldFailDueToInsufficientFunds() {
|
||||
final EntityWithIdAndVersion<Account> fromAccount= await(accountService.openAccount(new BigDecimal(150)));
|
||||
final EntityWithIdAndVersion<Account> fromAccount = await(accountService.openAccount("00000000-00000000", "My Account", new BigDecimal(150), ""));
|
||||
|
||||
final EntityWithIdAndVersion<Account> toAccount = await(accountService.openAccount(new BigDecimal(300)));
|
||||
final EntityWithIdAndVersion<Account> toAccount = await(accountService.openAccount("00000000-00000000", "My Account", new BigDecimal(300), ""));
|
||||
|
||||
final EntityWithIdAndVersion<MoneyTransfer> transaction = await(
|
||||
moneyTransferService.transferMoney(new TransferDetails(fromAccount.getEntityIdentifier(),
|
||||
toAccount.getEntityIdentifier(),
|
||||
moneyTransferService.transferMoney(new TransferDetails(fromAccount.getEntityId(),
|
||||
toAccount.getEntityId(),
|
||||
new BigDecimal(200))));
|
||||
|
||||
|
||||
eventually (
|
||||
new Producer<EntityWithMetadata<MoneyTransfer>>() {
|
||||
@Override
|
||||
public Observable<EntityWithMetadata<MoneyTransfer>> produce() {
|
||||
return (Observable<EntityWithMetadata<MoneyTransfer>>)eventStore.find(MoneyTransfer.class, transaction.getEntityIdentifier());
|
||||
}
|
||||
},
|
||||
new Verifier<EntityWithMetadata<MoneyTransfer>>() {
|
||||
@Override
|
||||
public void verify(EntityWithMetadata<MoneyTransfer> updatedTransaction) {
|
||||
Assert.assertEquals(TransferState.FAILED_DUE_TO_INSUFFICIENT_FUNDS, updatedTransaction.entity().getState());
|
||||
}
|
||||
});
|
||||
eventually(
|
||||
() -> eventStore.find(MoneyTransfer.class, transaction.getEntityId()),
|
||||
updatedTransaction -> Assert.assertEquals(TransferState.FAILED_DUE_TO_INSUFFICIENT_FUNDS, updatedTransaction.getEntity().getState()));
|
||||
|
||||
eventually (
|
||||
new Producer<EntityWithMetadata<Account>>() {
|
||||
@Override
|
||||
public Observable<EntityWithMetadata<Account>> produce() {
|
||||
return (Observable<EntityWithMetadata<Account>>)eventStore.find(Account.class, fromAccount.getEntityIdentifier());
|
||||
}
|
||||
},
|
||||
new Verifier<EntityWithMetadata<Account>>() {
|
||||
@Override
|
||||
public void verify(EntityWithMetadata<Account> account) {
|
||||
Assert.assertEquals(new BigDecimal(150), account.entity().getBalance());
|
||||
}
|
||||
});
|
||||
eventually(
|
||||
() -> eventStore.find(Account.class, fromAccount.getEntityId()),
|
||||
account -> Assert.assertEquals(new BigDecimal(150), account.getEntity().getBalance()));
|
||||
|
||||
eventually (
|
||||
new Producer<EntityWithMetadata<Account>>() {
|
||||
@Override
|
||||
public Observable<EntityWithMetadata<Account>> produce() {
|
||||
return (Observable<EntityWithMetadata<Account>>)eventStore.find(Account.class, toAccount.getEntityIdentifier());
|
||||
}
|
||||
},
|
||||
new Verifier<EntityWithMetadata<Account>>() {
|
||||
@Override
|
||||
public void verify(EntityWithMetadata<Account> account) {
|
||||
Assert.assertEquals(new BigDecimal(300), account.entity().getBalance());
|
||||
}
|
||||
});
|
||||
eventually(
|
||||
() -> eventStore.find(Account.class, toAccount.getEntityId()),
|
||||
account -> Assert.assertEquals(new BigDecimal(300), account.getEntity().getBalance()));
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.EntityWithIdAndVersion;
|
||||
import net.chrisrichardson.eventstore.EntityWithMetadata;
|
||||
import net.chrisrichardson.eventstore.EventStore;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.Account;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransfer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.TransferState;
|
||||
import io.eventuate.EntityWithIdAndVersion;
|
||||
import io.eventuate.EventuateAggregateStore;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.Account;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountQueryService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.transactions.TransferState;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.common.transactions.TransferDetails;
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Producer;
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Verifier;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransfer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransferService;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -18,9 +16,9 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.IntegrationTest;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import rx.Observable;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.await;
|
||||
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.eventually;
|
||||
@@ -37,7 +35,7 @@ public class AccountQuerySideIntegrationTest {
|
||||
private MoneyTransferService moneyTransferService;
|
||||
|
||||
@Autowired
|
||||
private EventStore eventStore;
|
||||
private EventuateAggregateStore eventStore;
|
||||
|
||||
@Autowired
|
||||
private AccountQueryService accountQueryService;
|
||||
@@ -45,54 +43,24 @@ public class AccountQuerySideIntegrationTest {
|
||||
@Test
|
||||
public void shouldUpdateQuerySide() throws Exception {
|
||||
|
||||
final EntityWithIdAndVersion<Account> fromAccount = await(accountService.openAccount(new BigDecimal(150)));
|
||||
final EntityWithIdAndVersion<Account> fromAccount = await(accountService.openAccount("00000000-00000000", "My Account", new BigDecimal(150), ""));
|
||||
|
||||
final EntityWithIdAndVersion<Account> toAccount = await(accountService.openAccount(new BigDecimal(300)));
|
||||
final EntityWithIdAndVersion<Account> toAccount = await(accountService.openAccount("00000000-00000000", "My Account", new BigDecimal(300), ""));
|
||||
|
||||
final EntityWithIdAndVersion<MoneyTransfer> transaction = await(
|
||||
moneyTransferService.transferMoney(new TransferDetails(fromAccount.getEntityIdentifier(),
|
||||
toAccount.getEntityIdentifier(),
|
||||
moneyTransferService.transferMoney(new TransferDetails(fromAccount.getEntityId(),
|
||||
toAccount.getEntityId(),
|
||||
new BigDecimal(80))));
|
||||
|
||||
eventually(
|
||||
new Producer<EntityWithMetadata<MoneyTransfer>>() {
|
||||
@Override
|
||||
public Observable<EntityWithMetadata<MoneyTransfer>> produce() {
|
||||
return eventStore.find(MoneyTransfer.class, transaction.getEntityIdentifier());
|
||||
}
|
||||
},
|
||||
new Verifier<EntityWithMetadata<MoneyTransfer>>() {
|
||||
@Override
|
||||
public void verify(EntityWithMetadata<MoneyTransfer> updatedTransaction) {
|
||||
Assert.assertEquals(TransferState.COMPLETED, updatedTransaction.entity().getState());
|
||||
}
|
||||
});
|
||||
() -> eventStore.find(MoneyTransfer.class, transaction.getEntityId()),
|
||||
updatedTransaction -> Assert.assertEquals(TransferState.COMPLETED, updatedTransaction.getEntity().getState()));
|
||||
|
||||
eventually(
|
||||
new Producer<AccountInfo>() {
|
||||
@Override
|
||||
public Observable<AccountInfo> produce() {
|
||||
return accountQueryService.findByAccountId(fromAccount.getEntityIdentifier());
|
||||
}
|
||||
},
|
||||
new Verifier<AccountInfo>() {
|
||||
@Override
|
||||
public void verify(AccountInfo accountInfo) {
|
||||
Assert.assertEquals(70*100, accountInfo.getBalance());
|
||||
}
|
||||
});
|
||||
() -> CompletableFuture.completedFuture(accountQueryService.findByAccountId(fromAccount.getEntityId())),
|
||||
accountInfo -> Assert.assertEquals(70 * 100, accountInfo.getBalance()));
|
||||
eventually(
|
||||
new Producer<AccountInfo>() {
|
||||
@Override
|
||||
public Observable<AccountInfo> produce() {
|
||||
return accountQueryService.findByAccountId(toAccount.getEntityIdentifier());
|
||||
}
|
||||
},
|
||||
new Verifier<AccountInfo>() {
|
||||
@Override
|
||||
public void verify(AccountInfo accountInfo) {
|
||||
Assert.assertEquals(380*100, accountInfo.getBalance());
|
||||
}
|
||||
});
|
||||
() -> CompletableFuture.completedFuture(accountQueryService.findByAccountId(toAccount.getEntityId())),
|
||||
accountInfo -> Assert.assertEquals(380 * 100, accountInfo.getBalance()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.accounts;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.accounts.AccountConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.backend.commandside.transactions.MoneyTransferConfiguration;
|
||||
import net.chrisrichardson.eventstore.jdbc.config.JdbcEventStoreConfiguration;
|
||||
import io.eventuate.javaclient.spring.jdbc.EmbeddedTestAggregateStoreConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsservice.backend.AccountsBackendConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.accountsviewservice.backend.AccountViewBackendConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.transactionsservice.backend.MoneyTransferBackendConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@Configuration
|
||||
@Import({AccountConfiguration.class, MoneyTransferConfiguration.class, JdbcEventStoreConfiguration.class, QuerySideAccountConfiguration.class})
|
||||
@Import({AccountsBackendConfiguration.class, MoneyTransferBackendConfiguration.class, EmbeddedTestAggregateStoreConfiguration.class,
|
||||
AccountViewBackendConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
public class AccountQuerySideTestConfiguration {
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.customers;
|
||||
|
||||
import io.eventuate.EntityWithIdAndVersion;
|
||||
import io.eventuate.EventuateAggregateStore;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.CustomerInfo;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.customersservice.backend.Customer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.customersservice.backend.CustomerService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.customersviewservice.backend.CustomerQueryService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.common.QuerySideCustomer;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.ToAccountInfo;
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Producer;
|
||||
import net.chrisrichardson.eventstorestore.javaexamples.testutil.Verifier;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.IntegrationTest;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.CustomersTestUtils.generateCustomerInfo;
|
||||
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.CustomersTestUtils.generateToAccountInfo;
|
||||
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.await;
|
||||
import static net.chrisrichardson.eventstorestore.javaexamples.testutil.TestUtil.eventually;
|
||||
|
||||
/**
|
||||
* Created by Main on 10.02.2016.
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = CustomerQuerySideTestConfiguration.class)
|
||||
@IntegrationTest
|
||||
public class CustomerQuerySideIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private CustomerService customerService;
|
||||
|
||||
@Autowired
|
||||
private CustomerQueryService customerQueryService;
|
||||
|
||||
@Autowired
|
||||
private EventuateAggregateStore eventStore;
|
||||
|
||||
@Test
|
||||
public void shouldCreateCustomerAndAddToAccount() throws Exception {
|
||||
CustomerInfo customerInfo = generateCustomerInfo();
|
||||
EntityWithIdAndVersion<Customer> customer = await(customerService.createCustomer(customerInfo));
|
||||
|
||||
ToAccountInfo toAccountInfo = generateToAccountInfo();
|
||||
EntityWithIdAndVersion<Customer> customerWithNewAccount = await(customerService.addToAccount(customer.getEntityId(), toAccountInfo));
|
||||
|
||||
eventually(
|
||||
new Producer<QuerySideCustomer>() {
|
||||
@Override
|
||||
public CompletableFuture<QuerySideCustomer> produce() {
|
||||
return customerQueryService.findByCustomerId(customer.getEntityId());
|
||||
}
|
||||
},
|
||||
new Verifier<QuerySideCustomer>() {
|
||||
@Override
|
||||
public void verify(QuerySideCustomer querySideCustomer) {
|
||||
Assert.assertEquals(customerInfo.getName(), querySideCustomer.getName());
|
||||
Assert.assertEquals(customerInfo.getSsn(), querySideCustomer.getSsn());
|
||||
Assert.assertEquals(customerInfo.getUserCredentials().getEmail(), querySideCustomer.getEmail());
|
||||
Assert.assertEquals(customerInfo.getPhoneNumber(), querySideCustomer.getPhoneNumber());
|
||||
Assert.assertEquals(customerInfo.getAddress(), querySideCustomer.getAddress());
|
||||
|
||||
Assert.assertNotNull(querySideCustomer.getToAccounts());
|
||||
Assert.assertFalse(querySideCustomer.getToAccounts().isEmpty());
|
||||
Assert.assertEquals(querySideCustomer.getToAccounts().get("11111111-11111111"), toAccountInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.backend.queryside.customers;
|
||||
|
||||
import io.eventuate.javaclient.spring.jdbc.EmbeddedTestAggregateStoreConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.customersservice.backend.CustomerBackendConfiguration;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.customersviewservice.backend.CustomerViewBackendConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@Configuration
|
||||
@Import({CustomerBackendConfiguration.class, EmbeddedTestAggregateStoreConfiguration.class, CustomerViewBackendConfiguration.class})
|
||||
@EnableAutoConfiguration
|
||||
public class CustomerQuerySideTestConfiguration {
|
||||
}
|
||||
6
java-spring/build-and-test-all-eventuate-local.sh
Executable file
6
java-spring/build-and-test-all-eventuate-local.sh
Executable file
@@ -0,0 +1,6 @@
|
||||
#! /bin/bash
|
||||
|
||||
export JAVA_OPTS="-Xmx128m -Xms128m"
|
||||
export EXTRA_INFRASTRUCTURE_SERVICES=cdcservice
|
||||
export EVENTUATE_LOCAL=yes
|
||||
../_build-and-test-all.sh -f docker-compose-eventuate-local.yml $* -P eventuateDriver=local
|
||||
@@ -1,3 +1,4 @@
|
||||
#! /bin/bash
|
||||
|
||||
export JAVA_OPTS="-Xmx128m -Xms128m"
|
||||
../_build-and-test-all.sh $*
|
||||
|
||||
@@ -23,6 +23,7 @@ subprojects {
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
jcenter()
|
||||
eventuateMavenRepoUrl.split(',').each { repoUrl -> maven { url repoUrl } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
|
||||
class EventuateDependencyPlugin implements Plugin<Project> {
|
||||
|
||||
@Override
|
||||
void apply(Project project) {
|
||||
project.dependencies {
|
||||
if (project.hasProperty("eventuateDriver") && project.property("eventuateDriver").equals("local")) {
|
||||
compile "io.eventuate.local.java:eventuate-local-java-jdbc:${project.eventuateLocalVersion}"
|
||||
compile "io.eventuate.local.java:eventuate-local-java-embedded-cdc-autoconfigure:${project.eventuateLocalVersion}"
|
||||
} else
|
||||
compile "io.eventuate.client.java:eventuate-client-java-http-stomp-spring:${project.eventuateClientVersion}"
|
||||
}
|
||||
}
|
||||
}
|
||||
12
java-spring/common-auth-web/build.gradle
Normal file
12
java-spring/common-auth-web/build.gradle
Normal file
@@ -0,0 +1,12 @@
|
||||
apply plugin: 'java'
|
||||
|
||||
dependencies {
|
||||
compile project(":common-auth")
|
||||
compile project(":common")
|
||||
|
||||
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
|
||||
compile "org.springframework.boot:spring-boot-starter-security:$springBootVersion"
|
||||
|
||||
testCompile "junit:junit:4.11"
|
||||
testCompile "org.springframework.boot:spring-boot-starter-test:$springBootVersion"
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.UserCredentials;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.CustomerAuthService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.model.ErrorResponse;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.model.User;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.common.QuerySideCustomer;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.token.Token;
|
||||
import org.springframework.security.core.token.TokenService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.springframework.web.bind.annotation.RequestMethod.GET;
|
||||
import static org.springframework.web.bind.annotation.RequestMethod.POST;
|
||||
|
||||
@RestController
|
||||
@Validated
|
||||
@RequestMapping("/api")
|
||||
public class AuthController {
|
||||
|
||||
@Autowired
|
||||
private TokenService tokenService;
|
||||
|
||||
@Autowired
|
||||
private CustomerAuthService customerAuthService;
|
||||
|
||||
private static ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@RequestMapping(value = "/login", method = POST)
|
||||
public ResponseEntity<QuerySideCustomer> doAuth(@RequestBody @Valid UserCredentials request) throws IOException {
|
||||
QuerySideCustomer customer = customerAuthService.findByEmailAndPassword(request.getEmail(), request.getPassword());
|
||||
|
||||
Token token = tokenService.allocateToken(objectMapper.writeValueAsString(new User(request.getEmail())));
|
||||
return ResponseEntity.status(HttpStatus.OK).header("access-token", token.getKey())
|
||||
.body(customer);
|
||||
}
|
||||
|
||||
@ResponseStatus(value = HttpStatus.NOT_FOUND)
|
||||
@ExceptionHandler({EmptyResultDataAccessException.class, IncorrectResultSizeDataAccessException.class})
|
||||
public ErrorResponse customersNotFound() {
|
||||
return new ErrorResponse("Customer not found");
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/user", method = GET)
|
||||
public ResponseEntity<QuerySideCustomer> getCurrentUser() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
return ResponseEntity.status(HttpStatus.OK).body(customerAuthService.findByEmail(auth.getName()));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.model;
|
||||
|
||||
import org.hibernate.validator.constraints.Email;
|
||||
import org.hibernate.validator.constraints.NotBlank;
|
||||
|
||||
public class AuthRequest {
|
||||
|
||||
@NotBlank
|
||||
@Email
|
||||
private String email;
|
||||
|
||||
@NotBlank
|
||||
private String password;
|
||||
|
||||
public AuthRequest() {
|
||||
}
|
||||
|
||||
public AuthRequest(String email, String password) {
|
||||
this.email = email;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.model;
|
||||
|
||||
/**
|
||||
* Created by Main on 17.02.2016.
|
||||
*/
|
||||
public class ErrorResponse {
|
||||
|
||||
private String message;
|
||||
|
||||
public ErrorResponse() {
|
||||
}
|
||||
|
||||
public ErrorResponse(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.Address;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.common.customers.Name;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.CustomerAuthService;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.model.User;
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.web.customers.queryside.common.QuerySideCustomer;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
import org.springframework.boot.test.IntegrationTest;
|
||||
import org.springframework.boot.test.SpringApplicationConfiguration;
|
||||
import org.springframework.dao.EmptyResultDataAccessException;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.token.DefaultToken;
|
||||
import org.springframework.security.core.token.TokenService;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.context.web.WebAppConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringApplicationConfiguration(classes = AuthControllerIntegrationTestConfiguration.class)
|
||||
@IntegrationTest
|
||||
@WebAppConfiguration
|
||||
public class AuthControllerIntegrationTest {
|
||||
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Mock
|
||||
TokenService tokenService;
|
||||
|
||||
@Mock
|
||||
CustomerAuthService customerAuthService;
|
||||
|
||||
@InjectMocks
|
||||
AuthController authController;
|
||||
|
||||
private static ObjectMapper om = new ObjectMapper();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
this.mockMvc = MockMvcBuilders.standaloneSetup(authController).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldLogin() throws Exception {
|
||||
when(customerAuthService.findByEmailAndPassword("my@email.com", "my_password")).thenReturn(new QuerySideCustomer("id", new Name("test", "test"), "my@email.com", "my_password", "ssn", "", new Address(), null));
|
||||
when(customerAuthService.findByEmailAndPassword("not_my@email.com", "not_my_password")).thenThrow(new EmptyResultDataAccessException(1));
|
||||
|
||||
when(tokenService.allocateToken(om.writeValueAsString(new User("my@email.com")))).thenReturn(new DefaultToken("key", System.currentTimeMillis(), ""));
|
||||
|
||||
mockMvc.perform(post("/api/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"email\" : \"my@email.com\", \"password\" : \"my_password\"}")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(result -> {
|
||||
assertTrue(result.getResponse().getContentAsString().contains("id"));
|
||||
assertTrue(result.getResponse().getContentAsString().contains("my@email.com"));
|
||||
assertTrue(result.getResponse().getContentAsString().contains("my_password"));
|
||||
});
|
||||
|
||||
mockMvc.perform(post("/api/login")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"email\" : \"not_my@email.com\", \"password\" : \"not_my_password\"}")
|
||||
.accept(MediaType.APPLICATION_JSON))
|
||||
.andExpect(status().isNotFound());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package net.chrisrichardson.eventstore.javaexamples.banking.commonauth.controller;
|
||||
|
||||
import net.chrisrichardson.eventstore.javaexamples.banking.commonauth.AuthConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@Configuration
|
||||
@Import(AuthConfiguration.class)
|
||||
@EnableAutoConfiguration
|
||||
public class AuthControllerIntegrationTestConfiguration {
|
||||
}
|
||||
15
java-spring/common-auth/build.gradle
Normal file
15
java-spring/common-auth/build.gradle
Normal file
@@ -0,0 +1,15 @@
|
||||
apply plugin: 'java'
|
||||
|
||||
dependencies {
|
||||
compile project(":common")
|
||||
compile project(":customers-query-side-common")
|
||||
|
||||
compile "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
|
||||
compile "org.springframework.boot:spring-boot-starter-data-mongodb:$springBootVersion"
|
||||
|
||||
compile "org.springframework.security:spring-security-config:4.0.2.RELEASE"
|
||||
compile "org.springframework.security:spring-security-web:4.0.2.RELEASE"
|
||||
|
||||
|
||||
testCompile "junit:junit:4.11"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user