├── .env ├── .github └── ISSUE_TEMPLATE │ └── bug-report-or-feature-request.md ├── .gitignore ├── .travis.yml ├── LICENCE ├── README.md ├── account-service ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── piggymetrics │ │ │ └── account │ │ │ ├── AccountApplication.java │ │ │ ├── client │ │ │ ├── AuthServiceClient.java │ │ │ ├── StatisticsServiceClient.java │ │ │ └── StatisticsServiceClientFallback.java │ │ │ ├── config │ │ │ └── ResourceServerConfig.java │ │ │ ├── controller │ │ │ ├── AccountController.java │ │ │ └── ErrorHandler.java │ │ │ ├── domain │ │ │ ├── Account.java │ │ │ ├── Currency.java │ │ │ ├── Item.java │ │ │ ├── Saving.java │ │ │ ├── TimePeriod.java │ │ │ └── User.java │ │ │ ├── repository │ │ │ └── AccountRepository.java │ │ │ └── service │ │ │ ├── AccountService.java │ │ │ ├── AccountServiceImpl.java │ │ │ └── security │ │ │ └── CustomUserInfoTokenServices.java │ └── resources │ │ └── bootstrap.yml │ └── test │ ├── java │ └── com │ │ └── piggymetrics │ │ └── account │ │ ├── AccountServiceApplicationTests.java │ │ ├── client │ │ └── StatisticsServiceClientFallbackTest.java │ │ ├── controller │ │ └── AccountControllerTest.java │ │ ├── repository │ │ └── AccountRepositoryTest.java │ │ └── service │ │ └── AccountServiceTest.java │ └── resources │ ├── application.yml │ └── bootstrap.yml ├── auth-service ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── piggymetrics │ │ │ └── auth │ │ │ ├── AuthApplication.java │ │ │ ├── config │ │ │ ├── OAuth2AuthorizationConfig.java │ │ │ └── WebSecurityConfig.java │ │ │ ├── controller │ │ │ └── UserController.java │ │ │ ├── domain │ │ │ └── User.java │ │ │ ├── repository │ │ │ └── UserRepository.java │ │ │ └── service │ │ │ ├── UserService.java │ │ │ ├── UserServiceImpl.java │ │ │ └── security │ │ │ └── MongoUserDetailsService.java │ └── resources │ │ └── bootstrap.yml │ └── test │ ├── java │ └── com │ │ └── piggymetrics │ │ └── auth │ │ ├── AuthServiceApplicationTests.java │ │ ├── controller │ │ └── UserControllerTest.java │ │ ├── repository │ │ └── UserRepositoryTest.java │ │ └── service │ │ ├── UserServiceTest.java │ │ └── security │ │ └── MongoUserDetailsServiceTest.java │ └── resources │ ├── application.yml │ └── bootstrap.yml ├── config ├── Dockerfile ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── piggymetrics │ │ └── config │ │ ├── ConfigApplication.java │ │ └── SecurityConfig.java │ └── resources │ ├── application.yml │ └── shared │ ├── account-service.yml │ ├── application.yml │ ├── auth-service.yml │ ├── gateway.yml │ ├── monitoring.yml │ ├── notification-service.yml │ ├── registry.yml │ ├── statistics-service.yml │ └── turbine-stream-service.yml ├── docker-compose.dev.yml ├── docker-compose.yml ├── gateway ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── piggymetrics │ │ │ └── gateway │ │ │ └── GatewayApplication.java │ └── resources │ │ ├── bootstrap.yml │ │ └── static │ │ ├── attribution.html │ │ ├── css │ │ ├── animation.css │ │ ├── launch.css │ │ └── style.css │ │ ├── fonts │ │ ├── museo-100 │ │ │ ├── museo-100.eot │ │ │ ├── museo-100.svg │ │ │ ├── museo-100.ttf │ │ │ └── museo-100.woff │ │ ├── museo-300 │ │ │ ├── museo-300.eot │ │ │ ├── museo-300.svg │ │ │ ├── museo-300.ttf │ │ │ └── museo-300.woff │ │ └── museo-500 │ │ │ ├── museo-500.eot │ │ │ ├── museo-500.svg │ │ │ ├── museo-500.ttf │ │ │ └── museo-500.woff │ │ ├── images │ │ ├── 1pagesprites.png │ │ ├── 1pagesprites@2x.png │ │ ├── github.gif │ │ ├── icons.png │ │ ├── icons@2x.png │ │ ├── linesbackground.png │ │ ├── linesbackground@2x.png │ │ ├── logo.gif │ │ ├── logo@2x.gif │ │ ├── logo_large.gif │ │ ├── logo_large@2x.gif │ │ ├── logotext.gif │ │ ├── logotext@2x.gif │ │ ├── logotext_large.gif │ │ ├── logotext_large@2x.gif │ │ ├── overview.png │ │ ├── piggy.gif │ │ ├── piggy@2x.gif │ │ ├── piggy_large.gif │ │ ├── piggy_large@2x.gif │ │ ├── preloader.gif │ │ ├── sprites.png │ │ ├── sprites@2x.png │ │ └── userpic.jpg │ │ ├── index.html │ │ └── js │ │ ├── dashboard.js │ │ ├── launch.js │ │ ├── lib │ │ ├── extrascripts.js │ │ ├── jquery.min.js │ │ └── touchscreens.js │ │ ├── login.js │ │ └── main.js │ └── test │ ├── java │ └── com │ │ └── piggymetrics │ │ └── gateway │ │ └── GatewayApplicationTests.java │ └── resources │ └── bootstrap.yml ├── mongodb ├── Dockerfile ├── dump │ └── account-service-dump.js └── init.sh ├── monitoring ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── piggymetrics │ │ │ └── monitoring │ │ │ └── MonitoringApplication.java │ └── resources │ │ └── bootstrap.yml │ └── test │ ├── java │ └── com │ │ └── piggymetrics │ │ └── monitoring │ │ └── MonitoringApplicationTests.java │ └── resources │ └── bootstrap.yml ├── notification-service ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── piggymetrics │ │ │ └── notification │ │ │ ├── NotificationServiceApplication.java │ │ │ ├── client │ │ │ └── AccountServiceClient.java │ │ │ ├── config │ │ │ └── ResourceServerConfig.java │ │ │ ├── controller │ │ │ └── RecipientController.java │ │ │ ├── domain │ │ │ ├── Frequency.java │ │ │ ├── NotificationSettings.java │ │ │ ├── NotificationType.java │ │ │ └── Recipient.java │ │ │ ├── repository │ │ │ ├── RecipientRepository.java │ │ │ └── converter │ │ │ │ ├── FrequencyReaderConverter.java │ │ │ │ └── FrequencyWriterConverter.java │ │ │ └── service │ │ │ ├── EmailService.java │ │ │ ├── EmailServiceImpl.java │ │ │ ├── NotificationService.java │ │ │ ├── NotificationServiceImpl.java │ │ │ ├── RecipientService.java │ │ │ └── RecipientServiceImpl.java │ └── resources │ │ └── bootstrap.yml │ └── test │ ├── java │ └── com │ │ └── piggymetrics │ │ └── notification │ │ ├── NotificationServiceApplicationTests.java │ │ ├── controller │ │ └── RecipientControllerTest.java │ │ ├── repository │ │ └── RecipientRepositoryTest.java │ │ └── service │ │ ├── EmailServiceImplTest.java │ │ ├── NotificationServiceImplTest.java │ │ └── RecipientServiceImplTest.java │ └── resources │ ├── application.yml │ └── bootstrap.yml ├── pom.xml ├── registry ├── Dockerfile ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── piggymetrics │ │ └── registry │ │ └── RegistryApplication.java │ └── resources │ └── bootstrap.yml ├── statistics-service ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── piggymetrics │ │ │ └── statistics │ │ │ ├── StatisticsApplication.java │ │ │ ├── client │ │ │ ├── ExchangeRatesClient.java │ │ │ └── ExchangeRatesClientFallback.java │ │ │ ├── config │ │ │ └── ResourceServerConfig.java │ │ │ ├── controller │ │ │ └── StatisticsController.java │ │ │ ├── domain │ │ │ ├── Account.java │ │ │ ├── Currency.java │ │ │ ├── ExchangeRatesContainer.java │ │ │ ├── Item.java │ │ │ ├── Saving.java │ │ │ ├── TimePeriod.java │ │ │ └── timeseries │ │ │ │ ├── DataPoint.java │ │ │ │ ├── DataPointId.java │ │ │ │ ├── ItemMetric.java │ │ │ │ └── StatisticMetric.java │ │ │ ├── repository │ │ │ ├── DataPointRepository.java │ │ │ └── converter │ │ │ │ ├── DataPointIdReaderConverter.java │ │ │ │ └── DataPointIdWriterConverter.java │ │ │ └── service │ │ │ ├── ExchangeRatesService.java │ │ │ ├── ExchangeRatesServiceImpl.java │ │ │ ├── StatisticsService.java │ │ │ ├── StatisticsServiceImpl.java │ │ │ └── security │ │ │ └── CustomUserInfoTokenServices.java │ └── resources │ │ └── bootstrap.yml │ └── test │ ├── java │ └── com │ │ └── piggymetrics │ │ └── statistics │ │ ├── StatisticsServiceApplicationTests.java │ │ ├── client │ │ └── ExchangeRatesClientTest.java │ │ ├── controller │ │ └── StatisticsControllerTest.java │ │ ├── repository │ │ └── DataPointRepositoryTest.java │ │ └── service │ │ ├── ExchangeRatesServiceImplTest.java │ │ └── StatisticsServiceImplTest.java │ └── resources │ ├── application.yml │ └── bootstrap.yml └── turbine-stream-service ├── Dockerfile ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── piggymetrics │ │ └── turbine │ │ └── TurbineStreamServiceApplication.java └── resources │ └── bootstrap.yml └── test ├── java └── com │ └── piggymetrics │ └── turbine │ └── TurbineStreamServiceApplicationTests.java └── resources └── bootstrap.yml /.env: -------------------------------------------------------------------------------- 1 | CONFIG_SERVICE_PASSWORD=password 2 | NOTIFICATION_SERVICE_PASSWORD=password 3 | STATISTICS_SERVICE_PASSWORD=password 4 | ACCOUNT_SERVICE_PASSWORD=password 5 | MONGODB_PASSWORD=password 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report-or-feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report or feature request 3 | about: Suggest an idea or report a bug 4 | 5 | --- 6 | 7 | - If you have troubles to run the components, or you'd like to ask a question - please use [PiggyMetrics gitter chat](https://gitter.im/sqshq/PiggyMetrics) 8 | 9 | - If you'd like to propose an improvement or report a bug, please provide a clear and concise description, attach logs and screenshots if possible. The issue should be ready to be implemented without any additional questions. PiggyMetrics is open source, nobody maintains it during the work hours. If you'd like the issue to be fixed, please consider to contibute your time to do that. 10 | 11 | Thank you. 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | .idea/ 3 | *.iml 4 | *.iws 5 | 6 | # Mac 7 | .DS_Store 8 | 9 | # Maven 10 | log/ 11 | target/ 12 | 13 | # Eclipse 14 | **/*.project 15 | **/*.classpath 16 | **/*.settings -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: required 3 | 4 | services: 5 | - docker 6 | 7 | language: java 8 | jdk: oraclejdk8 9 | 10 | env: 11 | global: 12 | - secure: "GeONVsTD48Y88CKoqupo/FC1Gy0eCrT1UNylvMzz5VYcLhWcUcu/d4870ZJznBqslGHbFaRc6VCXFLnbKNyR5Chgg127ouWOv/NFubJ5cO0UjHlBwoAxQ/SIrxG9W4B6Y2VeWRO0SIPHF6wUVeELiaPmIIe/jF15/SWJA926G2dF0+zTbn1KNWoVRi9pxZ0bIbEvVcVfHjWHsPtho916KRZ7ToV27f5E9DPLVMraK2HROOplJlXKNLuUor9xSmtOB/787yNrZmdsUaQDMHVfru9yEubfg4tF5ydoluB+JPzeUqIK2PrDgAiwmkEdoJFfbAOqwdy8vZkgpYd/t5vkhbOn3yA0QO1pnyhidx1YgyoJN3HwfMeVNQwUVg8jw7bltVe/DxsNo/AW11Tu7jKteuxewRHCxORzaUSmy5rnLq3AKIXT6h8Kq5VxP5tGbQypa7/z+Zu4vfOBdKGMiLllQ0vhhU0osgYzk/jT4KknuWQtOkX+QTS9iViIUwQgcn3kIMlz4eO83Mbt+IkNvgjF0DyE64mfV2ThTjXDV6959g3Nl/Fl95VMSTg95xtl0tbf733Lj+9HCIfLetlBz4ZzOqaDgD5fRL5HA8jR3FFHVdMd+dx/JELjSRHxO6ZFrCVG/2hBldIakO31/FbpODnDIkKO+86Oqz9DPKVnLJlmZp4=" # DOCKER_EMAIL 13 | - secure: "Gl6a03cI88dKHV4rjP1IkYqCdVe7IM0XNcEzFDCxmvXHWSpqlotntOYtgvtRKsYOzb0cfdQwgFuwj80glUbFX5vbX1y5r+qR5sQSrJVi61e8Pijn0MT7rE/d2gCJwaRTkR2lvtRRTIX41LA+aZ6F7+xFSd+ni82IlaXtDywQJWpCEmxcSqSUd5nVhHzH5JZmkYQmQ8fjxzGUhpeePfapYThPxXsHGxmJeoIlEDM1TEFtxVf3Zo9D10812uesa7EwNSKL6MOBU/Me7+liIHdRRpRwVmOjaFAZLeHsUfZQWcLWer0ODULov1U/YMdF860gV1X8PPYxAYNnWqevOGZIsYTX60yem/dCq90Lx7cIiK/TBZIS7x9k65QwP/shnO/RK8PPANBt8bJ4FBxmDPRMPRvgCPp4wQIYTyaiXd0M6BmK+LysS5cRgOOg7YF+ZRqKjNGY9rkNeGs0x8LIaE4Vz138tJCVJ0U+r2OOZFLCIu4dmvuOo1rWf4Hzh+Xt/nx8RrAwwqHKWRyayHkZEbQv2dNGcJsZyAzXsxA6NTDGQfKYloX5oY4qq3BMCkRoid9sLHoJvZrfkN6mjbZkEd2Ed3RzyTIxasmRUeD45vfr7go9ts1a6ppDrRYJWLi26pqyPhimTI0ljEYXp4QnK5JMya2y6H0wo/hJmzrqfXj4+C0=" # DOCKER_USER 14 | - secure: "VRlJyPOz7fUmtFdpTdO51BVcjKUGP5t7KF5bG7TSJPXsal7SSxjt6desLQ6zv31tKBp/SrgbnH4hloeAe38hL1I5gQKLPuNjOYhtolgLHlCO3aJiJ0vn0o6mgxOok4S/ul6EMy8VLbKzwt7GrruoaDNVadwc98NY+Grr1eLUzD5CVxa4luSahtKASheCtM29OQOj42Ivnc/MUUMYMymYa/zgIkROqI1ZbQK12NGwx13FzjjF2C9WyTR8IMFOeiuL/Ha8wxT2VeYExWkNw8TQKg35WT26axSoI92BBjPHY+l4IDQ0g8N176sAG52gbz4WXx60kRgqHrn+b5cjO/v1PknqXCqwWVrskm/mgGxJ4IVmpqa7ItDYtoVPzW4hyPsEHWyPMjii5280VdqVRueHyxSBH/uF3iQCDwR0hRdVnPPvgrt40/jbiD3pBhnDQErHCu54FA7uzfFT8LUvj3PgHn19KWAb4gKMpP0AZA3aDOD/3+db1x25ozhDXoCLPFk+kK0lp5mwTKka910lX4L25wp2P3RDdbGQTeVjBBp5+IxzfBuF7m2aEww7HgpB/TmHGaxo61cZfLFwcU2tfIQnPVRiomMqh85xFHIkDETNJJMdV1o+Im5maOzg3u5iy7E6dJec6jTYSCIzh6BemM+scYVZzLkYZVRyrUQkUj7ldoI=" # DOCKER_PASS 15 | - COMMIT=${TRAVIS_COMMIT::7} 16 | 17 | after_success: 18 | - bash <(curl -s https://codecov.io/bash) 19 | - docker login -u $DOCKER_USER -p $DOCKER_PASS 20 | 21 | #TAG 22 | - export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "latest"; else echo $TRAVIS_BRANCH ; fi` 23 | 24 | # CONFIG SERVICE 25 | - export CONFIG=sqshq/piggymetrics-config 26 | - docker build -t $CONFIG:$COMMIT ./config 27 | - docker tag $CONFIG:$COMMIT $CONFIG:$TAG 28 | - docker push $CONFIG 29 | 30 | # REGISTRY 31 | - export REGISTRY=sqshq/piggymetrics-registry 32 | - docker build -t $REGISTRY:$COMMIT ./registry 33 | - docker tag $REGISTRY:$COMMIT $REGISTRY:$TAG 34 | - docker push $REGISTRY 35 | 36 | # GATEWAY 37 | - export GATEWAY=sqshq/piggymetrics-gateway 38 | - docker build -t $GATEWAY:$COMMIT ./gateway 39 | - docker tag $GATEWAY:$COMMIT $GATEWAY:$TAG 40 | - docker push $GATEWAY 41 | 42 | # AUTH SERVICE 43 | - export AUTH_SERVICE=sqshq/piggymetrics-auth-service 44 | - docker build -t $AUTH_SERVICE:$COMMIT ./auth-service 45 | - docker tag $AUTH_SERVICE:$COMMIT $AUTH_SERVICE:$TAG 46 | - docker push $AUTH_SERVICE 47 | 48 | # ACCOUNT SERVICE 49 | - export ACCOUNT_SERVICE=sqshq/piggymetrics-account-service 50 | - docker build -t $ACCOUNT_SERVICE:$COMMIT ./account-service 51 | - docker tag $ACCOUNT_SERVICE:$COMMIT $ACCOUNT_SERVICE:$TAG 52 | - docker push $ACCOUNT_SERVICE 53 | 54 | # STATISTICS SERVICE 55 | - export STATISTICS_SERVICE=sqshq/piggymetrics-statistics-service 56 | - docker build -t $STATISTICS_SERVICE:$COMMIT ./statistics-service 57 | - docker tag $STATISTICS_SERVICE:$COMMIT $STATISTICS_SERVICE:$TAG 58 | - docker push $STATISTICS_SERVICE 59 | 60 | # NOTIFICATION_SERVICE 61 | - export NOTIFICATION_SERVICE=sqshq/piggymetrics-notification-service 62 | - docker build -t $NOTIFICATION_SERVICE:$COMMIT ./notification-service 63 | - docker tag $NOTIFICATION_SERVICE:$COMMIT $NOTIFICATION_SERVICE:$TAG 64 | - docker push $NOTIFICATION_SERVICE 65 | 66 | # MONITORING 67 | - export MONITORING=sqshq/piggymetrics-monitoring 68 | - docker build -t $MONITORING:$COMMIT ./monitoring 69 | - docker tag $MONITORING:$COMMIT $MONITORING:$TAG 70 | - docker push $MONITORING 71 | 72 | # TURBINE STREAM SERVICE 73 | - export TURBINE=sqshq/piggymetrics-turbine-stream-service 74 | - docker build -t $TURBINE:$COMMIT ./turbine-stream-service 75 | - docker tag $TURBINE:$COMMIT $TURBINE:$TAG 76 | - docker push $TURBINE 77 | 78 | # MONGO DB 79 | - export MONGO_DB=sqshq/piggymetrics-mongodb 80 | - docker build -t $MONGO_DB:$COMMIT ./mongodb 81 | - docker tag $MONGO_DB:$COMMIT $MONGO_DB:$TAG 82 | - docker push $MONGO_DB 83 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alexander Lukyanchikov, http://sqshq.com 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /account-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | MAINTAINER Alexander Lukyanchikov 3 | 4 | ADD ./target/account-service.jar /app/ 5 | CMD ["java", "-Xmx200m", "-jar", "/app/account-service.jar"] 6 | 7 | EXPOSE 6000 -------------------------------------------------------------------------------- /account-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | account-service 7 | 1.0-SNAPSHOT 8 | jar 9 | 10 | account-service 11 | 12 | 13 | com.piggymetrics 14 | piggymetrics 15 | 1.0-SNAPSHOT 16 | 17 | 18 | 19 | 20 | org.springframework.cloud 21 | spring-cloud-starter-oauth2 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-security 26 | 27 | 28 | org.springframework.cloud 29 | spring-cloud-starter-config 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-starter-openfeign 38 | 39 | 40 | org.springframework.cloud 41 | spring-cloud-starter-netflix-eureka-client 42 | 43 | 44 | org.springframework.cloud 45 | spring-cloud-starter-sleuth 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-data-mongodb 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-actuator 54 | 55 | 56 | org.springframework.cloud 57 | spring-cloud-starter-bus-amqp 58 | 59 | 60 | org.springframework.cloud 61 | spring-cloud-starter-netflix-hystrix 62 | 63 | 64 | org.springframework.cloud 65 | spring-cloud-netflix-hystrix-stream 66 | 67 | 68 | org.springframework.boot 69 | spring-boot-starter-test 70 | test 71 | 72 | 73 | de.flapdoodle.embed 74 | de.flapdoodle.embed.mongo 75 | 1.50.3 76 | test 77 | 78 | 79 | com.jayway.jsonpath 80 | json-path 81 | 2.2.0 82 | test 83 | 84 | 85 | 86 | 87 | 88 | 89 | org.springframework.boot 90 | spring-boot-maven-plugin 91 | 92 | account-service 93 | 94 | 95 | 96 | org.jacoco 97 | jacoco-maven-plugin 98 | 0.7.6.201602180812 99 | 100 | 101 | 102 | prepare-agent 103 | 104 | 105 | 106 | report 107 | test 108 | 109 | report 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/AccountApplication.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; 6 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 7 | import org.springframework.cloud.openfeign.EnableFeignClients; 8 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 9 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; 10 | 11 | @SpringBootApplication 12 | @EnableDiscoveryClient 13 | @EnableOAuth2Client 14 | @EnableFeignClients 15 | @EnableCircuitBreaker 16 | @EnableGlobalMethodSecurity(prePostEnabled = true) 17 | public class AccountApplication { 18 | 19 | public static void main(String[] args) { 20 | SpringApplication.run(AccountApplication.class, args); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/client/AuthServiceClient.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.client; 2 | 3 | import com.piggymetrics.account.domain.User; 4 | import org.springframework.cloud.openfeign.FeignClient; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | 9 | @FeignClient(name = "auth-service") 10 | public interface AuthServiceClient { 11 | 12 | @RequestMapping(method = RequestMethod.POST, value = "/uaa/users", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) 13 | void createUser(User user); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/client/StatisticsServiceClient.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.client; 2 | 3 | import com.piggymetrics.account.domain.Account; 4 | import org.springframework.cloud.openfeign.FeignClient; 5 | import org.springframework.http.MediaType; 6 | import org.springframework.web.bind.annotation.PathVariable; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | 10 | @FeignClient(name = "statistics-service", fallback = StatisticsServiceClientFallback.class) 11 | public interface StatisticsServiceClient { 12 | 13 | @RequestMapping(method = RequestMethod.PUT, value = "/statistics/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) 14 | void updateStatistics(@PathVariable("accountName") String accountName, Account account); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/client/StatisticsServiceClientFallback.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.client; 2 | 3 | import com.piggymetrics.account.domain.Account; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * @author cdov 10 | */ 11 | @Component 12 | public class StatisticsServiceClientFallback implements StatisticsServiceClient { 13 | private static final Logger LOGGER = LoggerFactory.getLogger(StatisticsServiceClientFallback.class); 14 | @Override 15 | public void updateStatistics(String accountName, Account account) { 16 | LOGGER.error("Error during update statistics for account: {}", accountName); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/config/ResourceServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.config; 2 | 3 | import com.piggymetrics.account.service.security.CustomUserInfoTokenServices; 4 | import feign.RequestInterceptor; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; 7 | import org.springframework.boot.context.properties.ConfigurationProperties; 8 | import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 12 | import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext; 13 | import org.springframework.security.oauth2.client.OAuth2RestTemplate; 14 | import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; 15 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 16 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 17 | import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; 18 | 19 | /** 20 | * @author cdov 21 | */ 22 | @Configuration 23 | @EnableResourceServer 24 | public class ResourceServerConfig extends ResourceServerConfigurerAdapter { 25 | 26 | private final ResourceServerProperties sso; 27 | 28 | @Autowired 29 | public ResourceServerConfig(ResourceServerProperties sso) { 30 | this.sso = sso; 31 | } 32 | 33 | @Bean 34 | @ConfigurationProperties(prefix = "security.oauth2.client") 35 | public ClientCredentialsResourceDetails clientCredentialsResourceDetails() { 36 | return new ClientCredentialsResourceDetails(); 37 | } 38 | 39 | @Bean 40 | public RequestInterceptor oauth2FeignRequestInterceptor(){ 41 | return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails()); 42 | } 43 | 44 | @Bean 45 | public OAuth2RestTemplate clientCredentialsRestTemplate() { 46 | return new OAuth2RestTemplate(clientCredentialsResourceDetails()); 47 | } 48 | 49 | @Bean 50 | public ResourceServerTokenServices tokenServices() { 51 | return new CustomUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId()); 52 | } 53 | 54 | @Override 55 | public void configure(HttpSecurity http) throws Exception { 56 | http.authorizeRequests() 57 | .antMatchers("/" , "/demo").permitAll() 58 | .anyRequest().authenticated(); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/controller/AccountController.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.controller; 2 | 3 | import com.piggymetrics.account.domain.Account; 4 | import com.piggymetrics.account.domain.User; 5 | import com.piggymetrics.account.service.AccountService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.access.prepost.PreAuthorize; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import javax.validation.Valid; 11 | import java.security.Principal; 12 | 13 | @RestController 14 | public class AccountController { 15 | 16 | @Autowired 17 | private AccountService accountService; 18 | 19 | @PreAuthorize("#oauth2.hasScope('server') or #name.equals('demo')") 20 | @RequestMapping(path = "/{name}", method = RequestMethod.GET) 21 | public Account getAccountByName(@PathVariable String name) { 22 | return accountService.findByName(name); 23 | } 24 | 25 | @RequestMapping(path = "/current", method = RequestMethod.GET) 26 | public Account getCurrentAccount(Principal principal) { 27 | return accountService.findByName(principal.getName()); 28 | } 29 | 30 | @RequestMapping(path = "/current", method = RequestMethod.PUT) 31 | public void saveCurrentAccount(Principal principal, @Valid @RequestBody Account account) { 32 | accountService.saveChanges(principal.getName(), account); 33 | } 34 | 35 | @RequestMapping(path = "/", method = RequestMethod.POST) 36 | public Account createNewAccount(@Valid @RequestBody User user) { 37 | return accountService.create(user); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/controller/ErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.controller; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.bind.annotation.ControllerAdvice; 7 | import org.springframework.web.bind.annotation.ExceptionHandler; 8 | import org.springframework.web.bind.annotation.ResponseStatus; 9 | 10 | @ControllerAdvice 11 | public class ErrorHandler { 12 | 13 | private final Logger log = LoggerFactory.getLogger(getClass()); 14 | 15 | // TODO add MethodArgumentNotValidException handler 16 | // TODO remove such general handler 17 | @ExceptionHandler(IllegalArgumentException.class) 18 | @ResponseStatus(HttpStatus.BAD_REQUEST) 19 | public void processValidationError(IllegalArgumentException e) { 20 | log.info("Returning HTTP 400 Bad Request", e); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/domain/Account.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.domain; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 4 | import org.hibernate.validator.constraints.Length; 5 | import org.springframework.data.annotation.Id; 6 | import org.springframework.data.mongodb.core.mapping.Document; 7 | 8 | import javax.validation.Valid; 9 | import javax.validation.constraints.NotNull; 10 | import java.util.Date; 11 | import java.util.List; 12 | 13 | @Document(collection = "accounts") 14 | @JsonIgnoreProperties(ignoreUnknown = true) 15 | public class Account { 16 | 17 | @Id 18 | private String name; 19 | 20 | private Date lastSeen; 21 | 22 | @Valid 23 | private List incomes; 24 | 25 | @Valid 26 | private List expenses; 27 | 28 | @Valid 29 | @NotNull 30 | private Saving saving; 31 | 32 | @Length(min = 0, max = 20_000) 33 | private String note; 34 | 35 | public String getName() { 36 | return name; 37 | } 38 | 39 | public void setName(String name) { 40 | this.name = name; 41 | } 42 | 43 | public Date getLastSeen() { 44 | return lastSeen; 45 | } 46 | 47 | public void setLastSeen(Date lastSeen) { 48 | this.lastSeen = lastSeen; 49 | } 50 | 51 | public List getIncomes() { 52 | return incomes; 53 | } 54 | 55 | public void setIncomes(List incomes) { 56 | this.incomes = incomes; 57 | } 58 | 59 | public List getExpenses() { 60 | return expenses; 61 | } 62 | 63 | public void setExpenses(List expenses) { 64 | this.expenses = expenses; 65 | } 66 | 67 | public Saving getSaving() { 68 | return saving; 69 | } 70 | 71 | public void setSaving(Saving saving) { 72 | this.saving = saving; 73 | } 74 | 75 | public String getNote() { 76 | return note; 77 | } 78 | 79 | public void setNote(String note) { 80 | this.note = note; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/domain/Currency.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.domain; 2 | 3 | public enum Currency { 4 | 5 | USD, EUR, RUB; 6 | 7 | public static Currency getDefault() { 8 | return USD; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/domain/Item.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.domain; 2 | 3 | import org.hibernate.validator.constraints.Length; 4 | 5 | import javax.validation.constraints.NotNull; 6 | import java.math.BigDecimal; 7 | 8 | public class Item { 9 | 10 | @NotNull 11 | @Length(min = 1, max = 20) 12 | private String title; 13 | 14 | @NotNull 15 | private BigDecimal amount; 16 | 17 | @NotNull 18 | private Currency currency; 19 | 20 | @NotNull 21 | private TimePeriod period; 22 | 23 | @NotNull 24 | private String icon; 25 | 26 | public String getTitle() { 27 | return title; 28 | } 29 | 30 | public void setTitle(String title) { 31 | this.title = title; 32 | } 33 | 34 | public BigDecimal getAmount() { 35 | return amount; 36 | } 37 | 38 | public void setAmount(BigDecimal amount) { 39 | this.amount = amount; 40 | } 41 | 42 | public Currency getCurrency() { 43 | return currency; 44 | } 45 | 46 | public void setCurrency(Currency currency) { 47 | this.currency = currency; 48 | } 49 | 50 | public TimePeriod getPeriod() { 51 | return period; 52 | } 53 | 54 | public void setPeriod(TimePeriod period) { 55 | this.period = period; 56 | } 57 | 58 | public String getIcon() { 59 | return icon; 60 | } 61 | 62 | public void setIcon(String icon) { 63 | this.icon = icon; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/domain/Saving.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.domain; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import java.math.BigDecimal; 5 | 6 | public class Saving { 7 | 8 | @NotNull 9 | private BigDecimal amount; 10 | 11 | @NotNull 12 | private Currency currency; 13 | 14 | @NotNull 15 | private BigDecimal interest; 16 | 17 | @NotNull 18 | private Boolean deposit; 19 | 20 | @NotNull 21 | private Boolean capitalization; 22 | 23 | public BigDecimal getAmount() { 24 | return amount; 25 | } 26 | 27 | public void setAmount(BigDecimal amount) { 28 | this.amount = amount; 29 | } 30 | 31 | public Currency getCurrency() { 32 | return currency; 33 | } 34 | 35 | public void setCurrency(Currency currency) { 36 | this.currency = currency; 37 | } 38 | 39 | public BigDecimal getInterest() { 40 | return interest; 41 | } 42 | 43 | public void setInterest(BigDecimal interest) { 44 | this.interest = interest; 45 | } 46 | 47 | public Boolean getDeposit() { 48 | return deposit; 49 | } 50 | 51 | public void setDeposit(Boolean deposit) { 52 | this.deposit = deposit; 53 | } 54 | 55 | public Boolean getCapitalization() { 56 | return capitalization; 57 | } 58 | 59 | public void setCapitalization(Boolean capitalization) { 60 | this.capitalization = capitalization; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/domain/TimePeriod.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.domain; 2 | 3 | public enum TimePeriod { 4 | 5 | YEAR, QUARTER, MONTH, DAY, HOUR 6 | 7 | } 8 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/domain/User.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.domain; 2 | 3 | import org.hibernate.validator.constraints.Length; 4 | 5 | import javax.validation.constraints.NotNull; 6 | 7 | public class User { 8 | 9 | @NotNull 10 | @Length(min = 3, max = 20) 11 | private String username; 12 | 13 | @NotNull 14 | @Length(min = 6, max = 40) 15 | private String password; 16 | 17 | public String getUsername() { 18 | return username; 19 | } 20 | 21 | public void setUsername(String username) { 22 | this.username = username; 23 | } 24 | 25 | public String getPassword() { 26 | return password; 27 | } 28 | 29 | public void setPassword(String password) { 30 | this.password = password; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.repository; 2 | 3 | import com.piggymetrics.account.domain.Account; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface AccountRepository extends CrudRepository { 9 | 10 | Account findByName(String name); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.service; 2 | 3 | import com.piggymetrics.account.domain.Account; 4 | import com.piggymetrics.account.domain.User; 5 | 6 | public interface AccountService { 7 | 8 | /** 9 | * Finds account by given name 10 | * 11 | * @param accountName 12 | * @return found account 13 | */ 14 | Account findByName(String accountName); 15 | 16 | /** 17 | * Checks if account with the same name already exists 18 | * Invokes Auth Service user creation 19 | * Creates new account with default parameters 20 | * 21 | * @param user 22 | * @return created account 23 | */ 24 | Account create(User user); 25 | 26 | /** 27 | * Validates and applies incoming account updates 28 | * Invokes Statistics Service update 29 | * 30 | * @param name 31 | * @param update 32 | */ 33 | void saveChanges(String name, Account update); 34 | } 35 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/piggymetrics/account/service/AccountServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.service; 2 | 3 | import com.piggymetrics.account.client.AuthServiceClient; 4 | import com.piggymetrics.account.client.StatisticsServiceClient; 5 | import com.piggymetrics.account.domain.Account; 6 | import com.piggymetrics.account.domain.Currency; 7 | import com.piggymetrics.account.domain.Saving; 8 | import com.piggymetrics.account.domain.User; 9 | import com.piggymetrics.account.repository.AccountRepository; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.util.Assert; 15 | 16 | import java.math.BigDecimal; 17 | import java.util.Date; 18 | 19 | @Service 20 | public class AccountServiceImpl implements AccountService { 21 | 22 | private final Logger log = LoggerFactory.getLogger(getClass()); 23 | 24 | @Autowired 25 | private StatisticsServiceClient statisticsClient; 26 | 27 | @Autowired 28 | private AuthServiceClient authClient; 29 | 30 | @Autowired 31 | private AccountRepository repository; 32 | 33 | /** 34 | * {@inheritDoc} 35 | */ 36 | @Override 37 | public Account findByName(String accountName) { 38 | Assert.hasLength(accountName); 39 | return repository.findByName(accountName); 40 | } 41 | 42 | /** 43 | * {@inheritDoc} 44 | */ 45 | @Override 46 | public Account create(User user) { 47 | 48 | Account existing = repository.findByName(user.getUsername()); 49 | Assert.isNull(existing, "account already exists: " + user.getUsername()); 50 | 51 | authClient.createUser(user); 52 | 53 | Saving saving = new Saving(); 54 | saving.setAmount(new BigDecimal(0)); 55 | saving.setCurrency(Currency.getDefault()); 56 | saving.setInterest(new BigDecimal(0)); 57 | saving.setDeposit(false); 58 | saving.setCapitalization(false); 59 | 60 | Account account = new Account(); 61 | account.setName(user.getUsername()); 62 | account.setLastSeen(new Date()); 63 | account.setSaving(saving); 64 | 65 | repository.save(account); 66 | 67 | log.info("new account has been created: " + account.getName()); 68 | 69 | return account; 70 | } 71 | 72 | /** 73 | * {@inheritDoc} 74 | */ 75 | @Override 76 | public void saveChanges(String name, Account update) { 77 | 78 | Account account = repository.findByName(name); 79 | Assert.notNull(account, "can't find account with name " + name); 80 | 81 | account.setIncomes(update.getIncomes()); 82 | account.setExpenses(update.getExpenses()); 83 | account.setSaving(update.getSaving()); 84 | account.setNote(update.getNote()); 85 | account.setLastSeen(new Date()); 86 | repository.save(account); 87 | 88 | log.debug("account {} changes has been saved", name); 89 | 90 | statisticsClient.updateStatistics(name, account); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /account-service/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: account-service 4 | cloud: 5 | config: 6 | uri: http://config:8888 7 | fail-fast: true 8 | password: ${CONFIG_SERVICE_PASSWORD} 9 | username: user 10 | -------------------------------------------------------------------------------- /account-service/src/test/java/com/piggymetrics/account/AccountServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class AccountServiceApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /account-service/src/test/java/com/piggymetrics/account/client/StatisticsServiceClientFallbackTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.client; 2 | 3 | import com.piggymetrics.account.domain.Account; 4 | import org.junit.Before; 5 | import org.junit.Rule; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.boot.test.rule.OutputCapture; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import static org.hamcrest.Matchers.containsString; 14 | 15 | /** 16 | * @author cdov 17 | */ 18 | @RunWith(SpringRunner.class) 19 | @SpringBootTest(properties = { 20 | "feign.hystrix.enabled=true" 21 | }) 22 | public class StatisticsServiceClientFallbackTest { 23 | @Autowired 24 | private StatisticsServiceClient statisticsServiceClient; 25 | 26 | @Rule 27 | public final OutputCapture outputCapture = new OutputCapture(); 28 | 29 | @Before 30 | public void setup() { 31 | outputCapture.reset(); 32 | } 33 | 34 | @Test 35 | public void testUpdateStatisticsWithFailFallback(){ 36 | statisticsServiceClient.updateStatistics("test", new Account()); 37 | 38 | outputCapture.expect(containsString("Error during update statistics for account: test")); 39 | 40 | } 41 | 42 | } 43 | 44 | -------------------------------------------------------------------------------- /account-service/src/test/java/com/piggymetrics/account/controller/AccountControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.common.collect.ImmutableList; 5 | import com.piggymetrics.account.domain.*; 6 | import com.piggymetrics.account.service.AccountService; 7 | import com.sun.security.auth.UserPrincipal; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.mockito.InjectMocks; 12 | import org.mockito.Mock; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 18 | 19 | import java.math.BigDecimal; 20 | import java.util.Date; 21 | 22 | import static org.mockito.Mockito.when; 23 | import static org.mockito.MockitoAnnotations.initMocks; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 27 | 28 | @RunWith(SpringRunner.class) 29 | @SpringBootTest 30 | public class AccountControllerTest { 31 | 32 | private static final ObjectMapper mapper = new ObjectMapper(); 33 | 34 | @InjectMocks 35 | private AccountController accountController; 36 | 37 | @Mock 38 | private AccountService accountService; 39 | 40 | private MockMvc mockMvc; 41 | 42 | @Before 43 | public void setup() { 44 | initMocks(this); 45 | this.mockMvc = MockMvcBuilders.standaloneSetup(accountController).build(); 46 | } 47 | 48 | @Test 49 | public void shouldGetAccountByName() throws Exception { 50 | 51 | final Account account = new Account(); 52 | account.setName("test"); 53 | 54 | when(accountService.findByName(account.getName())).thenReturn(account); 55 | 56 | mockMvc.perform(get("/" + account.getName())) 57 | .andExpect(jsonPath("$.name").value(account.getName())) 58 | .andExpect(status().isOk()); 59 | } 60 | 61 | @Test 62 | public void shouldGetCurrentAccount() throws Exception { 63 | 64 | final Account account = new Account(); 65 | account.setName("test"); 66 | 67 | when(accountService.findByName(account.getName())).thenReturn(account); 68 | 69 | mockMvc.perform(get("/current").principal(new UserPrincipal(account.getName()))) 70 | .andExpect(jsonPath("$.name").value(account.getName())) 71 | .andExpect(status().isOk()); 72 | } 73 | 74 | @Test 75 | public void shouldSaveCurrentAccount() throws Exception { 76 | 77 | Saving saving = new Saving(); 78 | saving.setAmount(new BigDecimal(1500)); 79 | saving.setCurrency(Currency.USD); 80 | saving.setInterest(new BigDecimal("3.32")); 81 | saving.setDeposit(true); 82 | saving.setCapitalization(false); 83 | 84 | Item grocery = new Item(); 85 | grocery.setTitle("Grocery"); 86 | grocery.setAmount(new BigDecimal(10)); 87 | grocery.setCurrency(Currency.USD); 88 | grocery.setPeriod(TimePeriod.DAY); 89 | grocery.setIcon("meal"); 90 | 91 | Item salary = new Item(); 92 | salary.setTitle("Salary"); 93 | salary.setAmount(new BigDecimal(9100)); 94 | salary.setCurrency(Currency.USD); 95 | salary.setPeriod(TimePeriod.MONTH); 96 | salary.setIcon("wallet"); 97 | 98 | final Account account = new Account(); 99 | account.setName("test"); 100 | account.setNote("test note"); 101 | account.setLastSeen(new Date()); 102 | account.setSaving(saving); 103 | account.setExpenses(ImmutableList.of(grocery)); 104 | account.setIncomes(ImmutableList.of(salary)); 105 | 106 | String json = mapper.writeValueAsString(account); 107 | 108 | mockMvc.perform(put("/current").principal(new UserPrincipal(account.getName())).contentType(MediaType.APPLICATION_JSON).content(json)) 109 | .andExpect(status().isOk()); 110 | } 111 | 112 | @Test 113 | public void shouldFailOnValidationTryingToSaveCurrentAccount() throws Exception { 114 | 115 | final Account account = new Account(); 116 | account.setName("test"); 117 | 118 | String json = mapper.writeValueAsString(account); 119 | 120 | mockMvc.perform(put("/current").principal(new UserPrincipal(account.getName())).contentType(MediaType.APPLICATION_JSON).content(json)) 121 | .andExpect(status().isBadRequest()); 122 | } 123 | 124 | @Test 125 | public void shouldRegisterNewAccount() throws Exception { 126 | 127 | final User user = new User(); 128 | user.setUsername("test"); 129 | user.setPassword("password"); 130 | 131 | String json = mapper.writeValueAsString(user); 132 | System.out.println(json); 133 | mockMvc.perform(post("/").principal(new UserPrincipal("test")).contentType(MediaType.APPLICATION_JSON).content(json)) 134 | .andExpect(status().isOk()); 135 | } 136 | 137 | @Test 138 | public void shouldFailOnValidationTryingToRegisterNewAccount() throws Exception { 139 | 140 | final User user = new User(); 141 | user.setUsername("t"); 142 | 143 | String json = mapper.writeValueAsString(user); 144 | 145 | mockMvc.perform(post("/").principal(new UserPrincipal("test")).contentType(MediaType.APPLICATION_JSON).content(json)) 146 | .andExpect(status().isBadRequest()); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /account-service/src/test/java/com/piggymetrics/account/repository/AccountRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.repository; 2 | 3 | import com.piggymetrics.account.domain.Account; 4 | import com.piggymetrics.account.domain.Currency; 5 | import com.piggymetrics.account.domain.Item; 6 | import com.piggymetrics.account.domain.Saving; 7 | import com.piggymetrics.account.domain.TimePeriod; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | 14 | import java.math.BigDecimal; 15 | import java.util.Arrays; 16 | import java.util.Date; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | 20 | @RunWith(SpringRunner.class) 21 | @DataMongoTest 22 | public class AccountRepositoryTest { 23 | 24 | @Autowired 25 | private AccountRepository repository; 26 | 27 | @Test 28 | public void shouldFindAccountByName() { 29 | 30 | Account stub = getStubAccount(); 31 | repository.save(stub); 32 | 33 | Account found = repository.findByName(stub.getName()); 34 | assertEquals(stub.getLastSeen(), found.getLastSeen()); 35 | assertEquals(stub.getNote(), found.getNote()); 36 | assertEquals(stub.getIncomes().size(), found.getIncomes().size()); 37 | assertEquals(stub.getExpenses().size(), found.getExpenses().size()); 38 | } 39 | 40 | private Account getStubAccount() { 41 | 42 | Saving saving = new Saving(); 43 | saving.setAmount(new BigDecimal(1500)); 44 | saving.setCurrency(Currency.USD); 45 | saving.setInterest(new BigDecimal("3.32")); 46 | saving.setDeposit(true); 47 | saving.setCapitalization(false); 48 | 49 | Item vacation = new Item(); 50 | vacation.setTitle("Vacation"); 51 | vacation.setAmount(new BigDecimal(3400)); 52 | vacation.setCurrency(Currency.EUR); 53 | vacation.setPeriod(TimePeriod.YEAR); 54 | vacation.setIcon("tourism"); 55 | 56 | Item grocery = new Item(); 57 | grocery.setTitle("Grocery"); 58 | grocery.setAmount(new BigDecimal(10)); 59 | grocery.setCurrency(Currency.USD); 60 | grocery.setPeriod(TimePeriod.DAY); 61 | grocery.setIcon("meal"); 62 | 63 | Item salary = new Item(); 64 | salary.setTitle("Salary"); 65 | salary.setAmount(new BigDecimal(9100)); 66 | salary.setCurrency(Currency.USD); 67 | salary.setPeriod(TimePeriod.MONTH); 68 | salary.setIcon("wallet"); 69 | 70 | Account account = new Account(); 71 | account.setName("test"); 72 | account.setNote("test note"); 73 | account.setLastSeen(new Date()); 74 | account.setSaving(saving); 75 | account.setExpenses(Arrays.asList(grocery, vacation)); 76 | account.setIncomes(Arrays.asList(salary)); 77 | 78 | return account; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /account-service/src/test/java/com/piggymetrics/account/service/AccountServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.account.service; 2 | 3 | import com.piggymetrics.account.client.AuthServiceClient; 4 | import com.piggymetrics.account.client.StatisticsServiceClient; 5 | import com.piggymetrics.account.domain.*; 6 | import com.piggymetrics.account.repository.AccountRepository; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | 12 | import java.math.BigDecimal; 13 | import java.util.Arrays; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | import static org.junit.Assert.assertNotNull; 17 | import static org.mockito.Mockito.*; 18 | import static org.mockito.MockitoAnnotations.initMocks; 19 | 20 | public class AccountServiceTest { 21 | 22 | @InjectMocks 23 | private AccountServiceImpl accountService; 24 | 25 | @Mock 26 | private StatisticsServiceClient statisticsClient; 27 | 28 | @Mock 29 | private AuthServiceClient authClient; 30 | 31 | @Mock 32 | private AccountRepository repository; 33 | 34 | @Before 35 | public void setup() { 36 | initMocks(this); 37 | } 38 | 39 | @Test 40 | public void shouldFindByName() { 41 | 42 | final Account account = new Account(); 43 | account.setName("test"); 44 | 45 | when(accountService.findByName(account.getName())).thenReturn(account); 46 | Account found = accountService.findByName(account.getName()); 47 | 48 | assertEquals(account, found); 49 | } 50 | 51 | @Test(expected = IllegalArgumentException.class) 52 | public void shouldFailWhenNameIsEmpty() { 53 | accountService.findByName(""); 54 | } 55 | 56 | @Test 57 | public void shouldCreateAccountWithGivenUser() { 58 | 59 | User user = new User(); 60 | user.setUsername("test"); 61 | 62 | Account account = accountService.create(user); 63 | 64 | assertEquals(user.getUsername(), account.getName()); 65 | assertEquals(0, account.getSaving().getAmount().intValue()); 66 | assertEquals(Currency.getDefault(), account.getSaving().getCurrency()); 67 | assertEquals(0, account.getSaving().getInterest().intValue()); 68 | assertEquals(false, account.getSaving().getDeposit()); 69 | assertEquals(false, account.getSaving().getCapitalization()); 70 | assertNotNull(account.getLastSeen()); 71 | 72 | verify(authClient, times(1)).createUser(user); 73 | verify(repository, times(1)).save(account); 74 | } 75 | 76 | @Test 77 | public void shouldSaveChangesWhenUpdatedAccountGiven() { 78 | 79 | Item grocery = new Item(); 80 | grocery.setTitle("Grocery"); 81 | grocery.setAmount(new BigDecimal(10)); 82 | grocery.setCurrency(Currency.USD); 83 | grocery.setPeriod(TimePeriod.DAY); 84 | grocery.setIcon("meal"); 85 | 86 | Item salary = new Item(); 87 | salary.setTitle("Salary"); 88 | salary.setAmount(new BigDecimal(9100)); 89 | salary.setCurrency(Currency.USD); 90 | salary.setPeriod(TimePeriod.MONTH); 91 | salary.setIcon("wallet"); 92 | 93 | Saving saving = new Saving(); 94 | saving.setAmount(new BigDecimal(1500)); 95 | saving.setCurrency(Currency.USD); 96 | saving.setInterest(new BigDecimal("3.32")); 97 | saving.setDeposit(true); 98 | saving.setCapitalization(false); 99 | 100 | final Account update = new Account(); 101 | update.setName("test"); 102 | update.setNote("test note"); 103 | update.setIncomes(Arrays.asList(salary)); 104 | update.setExpenses(Arrays.asList(grocery)); 105 | update.setSaving(saving); 106 | 107 | final Account account = new Account(); 108 | 109 | when(accountService.findByName("test")).thenReturn(account); 110 | accountService.saveChanges("test", update); 111 | 112 | assertEquals(update.getNote(), account.getNote()); 113 | assertNotNull(account.getLastSeen()); 114 | 115 | assertEquals(update.getSaving().getAmount(), account.getSaving().getAmount()); 116 | assertEquals(update.getSaving().getCurrency(), account.getSaving().getCurrency()); 117 | assertEquals(update.getSaving().getInterest(), account.getSaving().getInterest()); 118 | assertEquals(update.getSaving().getDeposit(), account.getSaving().getDeposit()); 119 | assertEquals(update.getSaving().getCapitalization(), account.getSaving().getCapitalization()); 120 | 121 | assertEquals(update.getExpenses().size(), account.getExpenses().size()); 122 | assertEquals(update.getIncomes().size(), account.getIncomes().size()); 123 | 124 | assertEquals(update.getExpenses().get(0).getTitle(), account.getExpenses().get(0).getTitle()); 125 | assertEquals(0, update.getExpenses().get(0).getAmount().compareTo(account.getExpenses().get(0).getAmount())); 126 | assertEquals(update.getExpenses().get(0).getCurrency(), account.getExpenses().get(0).getCurrency()); 127 | assertEquals(update.getExpenses().get(0).getPeriod(), account.getExpenses().get(0).getPeriod()); 128 | assertEquals(update.getExpenses().get(0).getIcon(), account.getExpenses().get(0).getIcon()); 129 | 130 | assertEquals(update.getIncomes().get(0).getTitle(), account.getIncomes().get(0).getTitle()); 131 | assertEquals(0, update.getIncomes().get(0).getAmount().compareTo(account.getIncomes().get(0).getAmount())); 132 | assertEquals(update.getIncomes().get(0).getCurrency(), account.getIncomes().get(0).getCurrency()); 133 | assertEquals(update.getIncomes().get(0).getPeriod(), account.getIncomes().get(0).getPeriod()); 134 | assertEquals(update.getIncomes().get(0).getIcon(), account.getIncomes().get(0).getIcon()); 135 | 136 | verify(repository, times(1)).save(account); 137 | verify(statisticsClient, times(1)).updateStatistics("test", account); 138 | } 139 | 140 | @Test(expected = IllegalArgumentException.class) 141 | public void shouldFailWhenNoAccountsExistedWithGivenName() { 142 | final Account update = new Account(); 143 | update.setIncomes(Arrays.asList(new Item())); 144 | update.setExpenses(Arrays.asList(new Item())); 145 | 146 | when(accountService.findByName("test")).thenReturn(null); 147 | accountService.saveChanges("test", update); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /account-service/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data: 3 | mongodb: 4 | database: piggymetrics 5 | port: 0 -------------------------------------------------------------------------------- /account-service/src/test/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | eureka: 2 | client: 3 | enabled: false -------------------------------------------------------------------------------- /auth-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | MAINTAINER Alexander Lukyanchikov 3 | 4 | ADD ./target/auth-service.jar /app/ 5 | CMD ["java", "-Xmx200m", "-jar", "/app/auth-service.jar"] 6 | 7 | EXPOSE 5000 -------------------------------------------------------------------------------- /auth-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | auth-service 7 | 1.0-SNAPSHOT 8 | jar 9 | 10 | auth-service 11 | 12 | 13 | com.piggymetrics 14 | piggymetrics 15 | 1.0-SNAPSHOT 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-data-mongodb 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-starter-config 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-security 30 | 31 | 32 | org.springframework.cloud 33 | spring-cloud-starter-oauth2 34 | 35 | 36 | org.springframework.boot 37 | spring-boot-starter-web 38 | 39 | 40 | org.springframework.cloud 41 | spring-cloud-starter-netflix-eureka-client 42 | 43 | 44 | org.springframework.cloud 45 | spring-cloud-starter-sleuth 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-test 50 | test 51 | 52 | 53 | de.flapdoodle.embed 54 | de.flapdoodle.embed.mongo 55 | 1.50.3 56 | test 57 | 58 | 59 | com.jayway.jsonpath 60 | json-path 61 | 2.2.0 62 | test 63 | 64 | 65 | 66 | 67 | 68 | 69 | org.springframework.boot 70 | spring-boot-maven-plugin 71 | 72 | auth-service 73 | 74 | 75 | 76 | org.jacoco 77 | jacoco-maven-plugin 78 | 0.7.6.201602180812 79 | 80 | 81 | 82 | prepare-agent 83 | 84 | 85 | 86 | report 87 | test 88 | 89 | report 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/piggymetrics/auth/AuthApplication.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 7 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 8 | 9 | @SpringBootApplication 10 | @EnableResourceServer 11 | @EnableDiscoveryClient 12 | @EnableGlobalMethodSecurity(prePostEnabled = true) 13 | public class AuthApplication { 14 | 15 | public static void main(String[] args) { 16 | SpringApplication.run(AuthApplication.class, args); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/piggymetrics/auth/config/OAuth2AuthorizationConfig.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.config; 2 | 3 | import com.piggymetrics.auth.service.security.MongoUserDetailsService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.beans.factory.annotation.Qualifier; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.env.Environment; 8 | import org.springframework.security.authentication.AuthenticationManager; 9 | import org.springframework.security.crypto.password.NoOpPasswordEncoder; 10 | import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; 11 | import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; 12 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; 13 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; 14 | import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; 15 | import org.springframework.security.oauth2.provider.token.TokenStore; 16 | import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore; 17 | 18 | /** 19 | * @author cdov 20 | */ 21 | @Configuration 22 | @EnableAuthorizationServer 23 | public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter { 24 | 25 | private TokenStore tokenStore = new InMemoryTokenStore(); 26 | private final String NOOP_PASSWORD_ENCODE = "{noop}"; 27 | 28 | @Autowired 29 | @Qualifier("authenticationManagerBean") 30 | private AuthenticationManager authenticationManager; 31 | 32 | @Autowired 33 | private MongoUserDetailsService userDetailsService; 34 | 35 | @Autowired 36 | private Environment env; 37 | 38 | @Override 39 | public void configure(ClientDetailsServiceConfigurer clients) throws Exception { 40 | 41 | // TODO persist clients details 42 | 43 | // @formatter:off 44 | clients.inMemory() 45 | .withClient("browser") 46 | .authorizedGrantTypes("refresh_token", "password") 47 | .scopes("ui") 48 | .and() 49 | .withClient("account-service") 50 | .secret(env.getProperty("ACCOUNT_SERVICE_PASSWORD")) 51 | .authorizedGrantTypes("client_credentials", "refresh_token") 52 | .scopes("server") 53 | .and() 54 | .withClient("statistics-service") 55 | .secret(env.getProperty("STATISTICS_SERVICE_PASSWORD")) 56 | .authorizedGrantTypes("client_credentials", "refresh_token") 57 | .scopes("server") 58 | .and() 59 | .withClient("notification-service") 60 | .secret(env.getProperty("NOTIFICATION_SERVICE_PASSWORD")) 61 | .authorizedGrantTypes("client_credentials", "refresh_token") 62 | .scopes("server"); 63 | // @formatter:on 64 | } 65 | 66 | @Override 67 | public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { 68 | endpoints 69 | .tokenStore(tokenStore) 70 | .authenticationManager(authenticationManager) 71 | .userDetailsService(userDetailsService); 72 | } 73 | 74 | @Override 75 | public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { 76 | oauthServer 77 | .tokenKeyAccess("permitAll()") 78 | .checkTokenAccess("isAuthenticated()") 79 | .passwordEncoder(NoOpPasswordEncoder.getInstance()); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/piggymetrics/auth/config/WebSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.config; 2 | 3 | import com.piggymetrics.auth.service.security.MongoUserDetailsService; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.authentication.AuthenticationManager; 8 | import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; 9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 11 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 12 | 13 | /** 14 | * @author cdov 15 | */ 16 | @Configuration 17 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 18 | 19 | @Autowired 20 | private MongoUserDetailsService userDetailsService; 21 | 22 | @Override 23 | protected void configure(HttpSecurity http) throws Exception { 24 | // @formatter:off 25 | http 26 | .authorizeRequests().anyRequest().authenticated() 27 | .and() 28 | .csrf().disable(); 29 | // @formatter:on 30 | } 31 | 32 | @Override 33 | protected void configure(AuthenticationManagerBuilder auth) throws Exception { 34 | auth.userDetailsService(userDetailsService) 35 | .passwordEncoder(new BCryptPasswordEncoder()); 36 | } 37 | 38 | @Override 39 | @Bean 40 | public AuthenticationManager authenticationManagerBean() throws Exception { 41 | return super.authenticationManagerBean(); 42 | } 43 | } -------------------------------------------------------------------------------- /auth-service/src/main/java/com/piggymetrics/auth/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.controller; 2 | 3 | import com.piggymetrics.auth.domain.User; 4 | import com.piggymetrics.auth.service.UserService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.security.access.prepost.PreAuthorize; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestMethod; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import javax.validation.Valid; 13 | import java.security.Principal; 14 | 15 | @RestController 16 | @RequestMapping("/users") 17 | public class UserController { 18 | 19 | @Autowired 20 | private UserService userService; 21 | 22 | @RequestMapping(value = "/current", method = RequestMethod.GET) 23 | public Principal getUser(Principal principal) { 24 | return principal; 25 | } 26 | 27 | @PreAuthorize("#oauth2.hasScope('server')") 28 | @RequestMapping(method = RequestMethod.POST) 29 | public void createUser(@Valid @RequestBody User user) { 30 | userService.create(user); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/piggymetrics/auth/domain/User.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.domain; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | import org.springframework.security.core.GrantedAuthority; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | 8 | import java.util.List; 9 | 10 | @Document(collection = "users") 11 | public class User implements UserDetails { 12 | 13 | @Id 14 | private String username; 15 | 16 | private String password; 17 | 18 | @Override 19 | public String getPassword() { 20 | return password; 21 | } 22 | 23 | @Override 24 | public String getUsername() { 25 | return username; 26 | } 27 | 28 | @Override 29 | public List getAuthorities() { 30 | return null; 31 | } 32 | 33 | public void setUsername(String username) { 34 | this.username = username; 35 | } 36 | 37 | public void setPassword(String password) { 38 | this.password = password; 39 | } 40 | 41 | @Override 42 | public boolean isAccountNonExpired() { 43 | return true; 44 | } 45 | 46 | @Override 47 | public boolean isAccountNonLocked() { 48 | return true; 49 | } 50 | 51 | @Override 52 | public boolean isCredentialsNonExpired() { 53 | return true; 54 | } 55 | 56 | @Override 57 | public boolean isEnabled() { 58 | return true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/piggymetrics/auth/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.repository; 2 | 3 | import com.piggymetrics.auth.domain.User; 4 | import org.springframework.data.repository.CrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | 7 | @Repository 8 | public interface UserRepository extends CrudRepository { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/piggymetrics/auth/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.service; 2 | 3 | import com.piggymetrics.auth.domain.User; 4 | 5 | public interface UserService { 6 | 7 | void create(User user); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/piggymetrics/auth/service/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.service; 2 | 3 | import com.piggymetrics.auth.domain.User; 4 | import com.piggymetrics.auth.repository.UserRepository; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.util.Assert; 11 | 12 | import java.util.Optional; 13 | 14 | @Service 15 | public class UserServiceImpl implements UserService { 16 | 17 | private final Logger log = LoggerFactory.getLogger(getClass()); 18 | 19 | private static final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); 20 | 21 | @Autowired 22 | private UserRepository repository; 23 | 24 | @Override 25 | public void create(User user) { 26 | 27 | Optional existing = repository.findById(user.getUsername()); 28 | existing.ifPresent(it-> {throw new IllegalArgumentException("user already exists: " + it.getUsername());}); 29 | 30 | String hash = encoder.encode(user.getPassword()); 31 | user.setPassword(hash); 32 | 33 | repository.save(user); 34 | 35 | log.info("new user has been created: {}", user.getUsername()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /auth-service/src/main/java/com/piggymetrics/auth/service/security/MongoUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.service.security; 2 | 3 | import com.piggymetrics.auth.repository.UserRepository; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | public class MongoUserDetailsService implements UserDetailsService { 12 | 13 | @Autowired 14 | private UserRepository repository; 15 | 16 | @Override 17 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 18 | 19 | return repository.findById(username).orElseThrow(()->new UsernameNotFoundException(username)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /auth-service/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: auth-service 4 | cloud: 5 | config: 6 | uri: http://config:8888 7 | fail-fast: true 8 | password: ${CONFIG_SERVICE_PASSWORD} 9 | username: user 10 | -------------------------------------------------------------------------------- /auth-service/src/test/java/com/piggymetrics/auth/AuthServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class AuthServiceApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /auth-service/src/test/java/com/piggymetrics/auth/controller/UserControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.piggymetrics.auth.domain.User; 5 | import com.piggymetrics.auth.service.UserService; 6 | import com.sun.security.auth.UserPrincipal; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.http.MediaType; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | import org.springframework.test.web.servlet.MockMvc; 16 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 17 | 18 | import static org.mockito.MockitoAnnotations.initMocks; 19 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 20 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 23 | 24 | @RunWith(SpringRunner.class) 25 | @SpringBootTest 26 | public class UserControllerTest { 27 | 28 | private static final ObjectMapper mapper = new ObjectMapper(); 29 | 30 | @InjectMocks 31 | private UserController accountController; 32 | 33 | @Mock 34 | private UserService userService; 35 | 36 | private MockMvc mockMvc; 37 | 38 | @Before 39 | public void setup() { 40 | initMocks(this); 41 | this.mockMvc = MockMvcBuilders.standaloneSetup(accountController).build(); 42 | } 43 | 44 | @Test 45 | public void shouldCreateNewUser() throws Exception { 46 | 47 | final User user = new User(); 48 | user.setUsername("test"); 49 | user.setPassword("password"); 50 | 51 | String json = mapper.writeValueAsString(user); 52 | 53 | mockMvc.perform(post("/users").contentType(MediaType.APPLICATION_JSON).content(json)) 54 | .andExpect(status().isOk()); 55 | } 56 | 57 | @Test 58 | public void shouldFailWhenUserIsNotValid() throws Exception { 59 | 60 | final User user = new User(); 61 | user.setUsername("t"); 62 | user.setPassword("p"); 63 | 64 | mockMvc.perform(post("/users")) 65 | .andExpect(status().isBadRequest()); 66 | } 67 | 68 | @Test 69 | public void shouldReturnCurrentUser() throws Exception { 70 | mockMvc.perform(get("/users/current").principal(new UserPrincipal("test"))) 71 | .andExpect(jsonPath("$.name").value("test")) 72 | .andExpect(status().isOk()); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /auth-service/src/test/java/com/piggymetrics/auth/repository/UserRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.repository; 2 | 3 | import com.piggymetrics.auth.domain.User; 4 | import com.piggymetrics.auth.service.security.MongoUserDetailsService; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration; 9 | import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import java.util.Optional; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | import static org.junit.Assert.assertTrue; 17 | 18 | @RunWith(SpringRunner.class) 19 | @DataMongoTest 20 | public class UserRepositoryTest { 21 | 22 | @Autowired 23 | private UserRepository repository; 24 | 25 | @Test 26 | public void shouldSaveAndFindUserByName() { 27 | 28 | User user = new User(); 29 | user.setUsername("name"); 30 | user.setPassword("password"); 31 | repository.save(user); 32 | 33 | Optional found = repository.findById(user.getUsername()); 34 | assertTrue(found.isPresent()); 35 | assertEquals(user.getUsername(), found.get().getUsername()); 36 | assertEquals(user.getPassword(), found.get().getPassword()); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /auth-service/src/test/java/com/piggymetrics/auth/service/UserServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.service; 2 | 3 | import com.piggymetrics.auth.domain.User; 4 | import com.piggymetrics.auth.repository.UserRepository; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.InjectMocks; 8 | import org.mockito.Mock; 9 | 10 | import java.util.Optional; 11 | 12 | import static org.mockito.Mockito.*; 13 | import static org.mockito.MockitoAnnotations.initMocks; 14 | 15 | public class UserServiceTest { 16 | 17 | @InjectMocks 18 | private UserServiceImpl userService; 19 | 20 | @Mock 21 | private UserRepository repository; 22 | 23 | @Before 24 | public void setup() { 25 | initMocks(this); 26 | } 27 | 28 | @Test 29 | public void shouldCreateUser() { 30 | 31 | User user = new User(); 32 | user.setUsername("name"); 33 | user.setPassword("password"); 34 | 35 | userService.create(user); 36 | verify(repository, times(1)).save(user); 37 | } 38 | 39 | @Test(expected = IllegalArgumentException.class) 40 | public void shouldFailWhenUserAlreadyExists() { 41 | 42 | User user = new User(); 43 | user.setUsername("name"); 44 | user.setPassword("password"); 45 | 46 | when(repository.findById(user.getUsername())).thenReturn(Optional.of(new User())); 47 | userService.create(user); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /auth-service/src/test/java/com/piggymetrics/auth/service/security/MongoUserDetailsServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.auth.service.security; 2 | 3 | import com.piggymetrics.auth.domain.User; 4 | import com.piggymetrics.auth.repository.UserRepository; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.InjectMocks; 8 | import org.mockito.Mock; 9 | import org.springframework.security.core.userdetails.UserDetails; 10 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 11 | 12 | import java.util.Optional; 13 | 14 | import static org.junit.Assert.assertEquals; 15 | import static org.mockito.Matchers.any; 16 | import static org.mockito.Mockito.when; 17 | import static org.mockito.MockitoAnnotations.initMocks; 18 | 19 | public class MongoUserDetailsServiceTest { 20 | 21 | @InjectMocks 22 | private MongoUserDetailsService service; 23 | 24 | @Mock 25 | private UserRepository repository; 26 | 27 | @Before 28 | public void setup() { 29 | initMocks(this); 30 | } 31 | 32 | @Test 33 | public void shouldLoadByUsernameWhenUserExists() { 34 | 35 | final User user = new User(); 36 | 37 | when(repository.findById(any())).thenReturn(Optional.of(user)); 38 | UserDetails loaded = service.loadUserByUsername("name"); 39 | 40 | assertEquals(user, loaded); 41 | } 42 | 43 | @Test(expected = UsernameNotFoundException.class) 44 | public void shouldFailToLoadByUsernameWhenUserNotExists() { 45 | service.loadUserByUsername("name"); 46 | } 47 | } -------------------------------------------------------------------------------- /auth-service/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data: 3 | mongodb: 4 | database: piggymetrics 5 | port: 0 -------------------------------------------------------------------------------- /auth-service/src/test/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | eureka: 2 | client: 3 | enabled: false -------------------------------------------------------------------------------- /config/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | MAINTAINER Alexander Lukyanchikov 3 | 4 | ADD ./target/config.jar /app/ 5 | CMD ["java", "-Xmx200m", "-jar", "/app/config.jar"] 6 | 7 | HEALTHCHECK --interval=30s --timeout=30s CMD curl -f http://localhost:8888/actuator/health || exit 1 8 | 9 | EXPOSE 8888 -------------------------------------------------------------------------------- /config/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | config 7 | 1.0.0-SNAPSHOT 8 | jar 9 | 10 | config 11 | Configuration Server 12 | 13 | 14 | com.piggymetrics 15 | piggymetrics 16 | 1.0-SNAPSHOT 17 | 18 | 19 | 20 | 21 | org.springframework.cloud 22 | spring-cloud-config-server 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-security 27 | 28 | 29 | 30 | 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-maven-plugin 35 | 36 | config 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /config/src/main/java/com/piggymetrics/config/ConfigApplication.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.config; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.config.server.EnableConfigServer; 6 | 7 | @SpringBootApplication 8 | @EnableConfigServer 9 | public class ConfigApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(ConfigApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /config/src/main/java/com/piggymetrics/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 6 | 7 | /** 8 | * @author cdov 9 | */ 10 | @Configuration 11 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 12 | 13 | @Override 14 | protected void configure(HttpSecurity http) throws Exception { 15 | http.csrf().disable(); 16 | http 17 | .authorizeRequests() 18 | .antMatchers("/actuator/**").permitAll() 19 | .anyRequest().authenticated() 20 | .and() 21 | .httpBasic() 22 | ; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | cloud: 3 | config: 4 | server: 5 | native: 6 | search-locations: classpath:/shared 7 | profiles: 8 | active: native 9 | security: 10 | user: 11 | password: ${CONFIG_SERVICE_PASSWORD} 12 | 13 | server: 14 | port: 8888 15 | 16 | -------------------------------------------------------------------------------- /config/src/main/resources/shared/account-service.yml: -------------------------------------------------------------------------------- 1 | security: 2 | oauth2: 3 | client: 4 | clientId: account-service 5 | clientSecret: ${ACCOUNT_SERVICE_PASSWORD} 6 | accessTokenUri: http://auth-service:5000/uaa/oauth/token 7 | grant-type: client_credentials 8 | scope: server 9 | 10 | spring: 11 | data: 12 | mongodb: 13 | host: account-mongodb 14 | username: user 15 | password: ${MONGODB_PASSWORD} 16 | database: piggymetrics 17 | port: 27017 18 | 19 | server: 20 | servlet: 21 | context-path: /accounts 22 | port: 6000 23 | 24 | feign: 25 | hystrix: 26 | enabled: true -------------------------------------------------------------------------------- /config/src/main/resources/shared/application.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | org.springframework.security: INFO 4 | 5 | hystrix: 6 | command: 7 | default: 8 | execution: 9 | isolation: 10 | thread: 11 | timeoutInMilliseconds: 10000 12 | 13 | eureka: 14 | instance: 15 | prefer-ip-address: true 16 | client: 17 | serviceUrl: 18 | defaultZone: http://registry:8761/eureka/ 19 | 20 | security: 21 | oauth2: 22 | resource: 23 | user-info-uri: http://auth-service:5000/uaa/users/current 24 | 25 | spring: 26 | rabbitmq: 27 | host: rabbitmq -------------------------------------------------------------------------------- /config/src/main/resources/shared/auth-service.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data: 3 | mongodb: 4 | host: auth-mongodb 5 | username: user 6 | password: ${MONGODB_PASSWORD} 7 | database: piggymetrics 8 | port: 27017 9 | 10 | server: 11 | servlet: 12 | context-path: /uaa 13 | port: 5000 14 | -------------------------------------------------------------------------------- /config/src/main/resources/shared/gateway.yml: -------------------------------------------------------------------------------- 1 | hystrix: 2 | command: 3 | default: 4 | execution: 5 | isolation: 6 | thread: 7 | timeoutInMilliseconds: 20000 8 | 9 | ribbon: 10 | ReadTimeout: 20000 11 | ConnectTimeout: 20000 12 | 13 | zuul: 14 | ignoredServices: '*' 15 | host: 16 | connect-timeout-millis: 20000 17 | socket-timeout-millis: 20000 18 | 19 | routes: 20 | auth-service: 21 | path: /uaa/** 22 | url: http://auth-service:5000 23 | stripPrefix: false 24 | sensitiveHeaders: 25 | 26 | account-service: 27 | path: /accounts/** 28 | serviceId: account-service 29 | stripPrefix: false 30 | sensitiveHeaders: 31 | 32 | statistics-service: 33 | path: /statistics/** 34 | serviceId: statistics-service 35 | stripPrefix: false 36 | sensitiveHeaders: 37 | 38 | notification-service: 39 | path: /notifications/** 40 | serviceId: notification-service 41 | stripPrefix: false 42 | sensitiveHeaders: 43 | 44 | server: 45 | port: 4000 46 | -------------------------------------------------------------------------------- /config/src/main/resources/shared/monitoring.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/config/src/main/resources/shared/monitoring.yml -------------------------------------------------------------------------------- /config/src/main/resources/shared/notification-service.yml: -------------------------------------------------------------------------------- 1 | security: 2 | oauth2: 3 | client: 4 | clientId: notification-service 5 | clientSecret: ${NOTIFICATION_SERVICE_PASSWORD} 6 | accessTokenUri: http://auth-service:5000/uaa/oauth/token 7 | grant-type: client_credentials 8 | scope: server 9 | 10 | server: 11 | servlet: 12 | context-path: /notifications 13 | port: 8000 14 | 15 | remind: 16 | cron: 0 0 0 * * * 17 | email: 18 | text: "Hey, {0}! We''ve missed you here on PiggyMetrics. It''s time to check your budget statistics.\r\n\r\nCheers,\r\nPiggyMetrics team" 19 | subject: PiggyMetrics reminder 20 | 21 | backup: 22 | cron: 0 0 12 * * * 23 | email: 24 | text: "Howdy, {0}. Your account backup is ready.\r\n\r\nCheers,\r\nPiggyMetrics team" 25 | subject: PiggyMetrics account backup 26 | attachment: backup.json 27 | 28 | spring: 29 | data: 30 | mongodb: 31 | host: notification-mongodb 32 | username: user 33 | password: ${MONGODB_PASSWORD} 34 | database: piggymetrics 35 | port: 27017 36 | mail: 37 | host: smtp.gmail.com 38 | port: 465 39 | username: dev-user 40 | password: dev-password 41 | properties: 42 | mail: 43 | smtp: 44 | auth: true 45 | socketFactory: 46 | port: 465 47 | class: javax.net.ssl.SSLSocketFactory 48 | fallback: false 49 | ssl: 50 | enable: true 51 | -------------------------------------------------------------------------------- /config/src/main/resources/shared/registry.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8761 -------------------------------------------------------------------------------- /config/src/main/resources/shared/statistics-service.yml: -------------------------------------------------------------------------------- 1 | security: 2 | oauth2: 3 | client: 4 | clientId: statistics-service 5 | clientSecret: ${STATISTICS_SERVICE_PASSWORD} 6 | accessTokenUri: http://auth-service:5000/uaa/oauth/token 7 | grant-type: client_credentials 8 | scope: server 9 | 10 | spring: 11 | data: 12 | mongodb: 13 | host: statistics-mongodb 14 | username: user 15 | password: ${MONGODB_PASSWORD} 16 | database: piggymetrics 17 | port: 27017 18 | 19 | server: 20 | servlet: 21 | context-path: /statistics 22 | port: 7000 23 | 24 | rates: 25 | url: https://api.exchangeratesapi.io -------------------------------------------------------------------------------- /config/src/main/resources/shared/turbine-stream-service.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/config/src/main/resources/shared/turbine-stream-service.yml -------------------------------------------------------------------------------- /docker-compose.dev.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | services: 3 | rabbitmq: 4 | ports: 5 | - 5672:5672 6 | 7 | config: 8 | build: config 9 | ports: 10 | - 8888:8888 11 | 12 | registry: 13 | build: registry 14 | 15 | gateway: 16 | build: gateway 17 | 18 | auth-service: 19 | build: auth-service 20 | ports: 21 | - 5000:5000 22 | 23 | auth-mongodb: 24 | build: mongodb 25 | ports: 26 | - 25000:27017 27 | 28 | account-service: 29 | build: account-service 30 | ports: 31 | - 6000:6000 32 | 33 | account-mongodb: 34 | build: mongodb 35 | ports: 36 | - 26000:27017 37 | 38 | statistics-service: 39 | build: statistics-service 40 | ports: 41 | - 7000:7000 42 | 43 | statistics-mongodb: 44 | build: mongodb 45 | ports: 46 | - 27000:27017 47 | 48 | notification-service: 49 | build: notification-service 50 | ports: 51 | - 8000:8000 52 | 53 | notification-mongodb: 54 | build: mongodb 55 | ports: 56 | - 28000:27017 57 | 58 | monitoring: 59 | build: monitoring 60 | 61 | turbine-stream-service: 62 | build: turbine-stream-service -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | services: 3 | rabbitmq: 4 | image: rabbitmq:3-management 5 | restart: always 6 | ports: 7 | - 15672:15672 8 | logging: 9 | options: 10 | max-size: "10m" 11 | max-file: "10" 12 | 13 | config: 14 | environment: 15 | CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD 16 | image: sqshq/piggymetrics-config 17 | restart: always 18 | logging: 19 | options: 20 | max-size: "10m" 21 | max-file: "10" 22 | 23 | registry: 24 | environment: 25 | CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD 26 | image: sqshq/piggymetrics-registry 27 | restart: always 28 | depends_on: 29 | config: 30 | condition: service_healthy 31 | ports: 32 | - 8761:8761 33 | logging: 34 | options: 35 | max-size: "10m" 36 | max-file: "10" 37 | 38 | gateway: 39 | environment: 40 | CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD 41 | image: sqshq/piggymetrics-gateway 42 | restart: always 43 | depends_on: 44 | config: 45 | condition: service_healthy 46 | ports: 47 | - 80:4000 48 | logging: 49 | options: 50 | max-size: "10m" 51 | max-file: "10" 52 | 53 | auth-service: 54 | environment: 55 | CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD 56 | NOTIFICATION_SERVICE_PASSWORD: $NOTIFICATION_SERVICE_PASSWORD 57 | STATISTICS_SERVICE_PASSWORD: $STATISTICS_SERVICE_PASSWORD 58 | ACCOUNT_SERVICE_PASSWORD: $ACCOUNT_SERVICE_PASSWORD 59 | MONGODB_PASSWORD: $MONGODB_PASSWORD 60 | image: sqshq/piggymetrics-auth-service 61 | restart: always 62 | depends_on: 63 | config: 64 | condition: service_healthy 65 | logging: 66 | options: 67 | max-size: "10m" 68 | max-file: "10" 69 | 70 | auth-mongodb: 71 | environment: 72 | MONGODB_PASSWORD: $MONGODB_PASSWORD 73 | image: sqshq/piggymetrics-mongodb 74 | restart: always 75 | logging: 76 | options: 77 | max-size: "10m" 78 | max-file: "10" 79 | 80 | account-service: 81 | environment: 82 | CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD 83 | ACCOUNT_SERVICE_PASSWORD: $ACCOUNT_SERVICE_PASSWORD 84 | MONGODB_PASSWORD: $MONGODB_PASSWORD 85 | image: sqshq/piggymetrics-account-service 86 | restart: always 87 | depends_on: 88 | config: 89 | condition: service_healthy 90 | logging: 91 | options: 92 | max-size: "10m" 93 | max-file: "10" 94 | 95 | account-mongodb: 96 | environment: 97 | INIT_DUMP: account-service-dump.js 98 | MONGODB_PASSWORD: $MONGODB_PASSWORD 99 | image: sqshq/piggymetrics-mongodb 100 | restart: always 101 | logging: 102 | options: 103 | max-size: "10m" 104 | max-file: "10" 105 | 106 | statistics-service: 107 | environment: 108 | CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD 109 | MONGODB_PASSWORD: $MONGODB_PASSWORD 110 | STATISTICS_SERVICE_PASSWORD: $STATISTICS_SERVICE_PASSWORD 111 | image: sqshq/piggymetrics-statistics-service 112 | restart: always 113 | depends_on: 114 | config: 115 | condition: service_healthy 116 | logging: 117 | options: 118 | max-size: "10m" 119 | max-file: "10" 120 | 121 | statistics-mongodb: 122 | environment: 123 | MONGODB_PASSWORD: $MONGODB_PASSWORD 124 | image: sqshq/piggymetrics-mongodb 125 | restart: always 126 | logging: 127 | options: 128 | max-size: "10m" 129 | max-file: "10" 130 | 131 | notification-service: 132 | environment: 133 | CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD 134 | MONGODB_PASSWORD: $MONGODB_PASSWORD 135 | NOTIFICATION_SERVICE_PASSWORD: $NOTIFICATION_SERVICE_PASSWORD 136 | image: sqshq/piggymetrics-notification-service 137 | restart: always 138 | depends_on: 139 | config: 140 | condition: service_healthy 141 | logging: 142 | options: 143 | max-size: "10m" 144 | max-file: "10" 145 | 146 | notification-mongodb: 147 | image: sqshq/piggymetrics-mongodb 148 | restart: always 149 | environment: 150 | MONGODB_PASSWORD: $MONGODB_PASSWORD 151 | logging: 152 | options: 153 | max-size: "10m" 154 | max-file: "10" 155 | 156 | monitoring: 157 | environment: 158 | CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD 159 | image: sqshq/piggymetrics-monitoring 160 | restart: always 161 | depends_on: 162 | config: 163 | condition: service_healthy 164 | ports: 165 | - 9000:8080 166 | logging: 167 | options: 168 | max-size: "10m" 169 | max-file: "10" 170 | 171 | turbine-stream-service: 172 | environment: 173 | CONFIG_SERVICE_PASSWORD: $CONFIG_SERVICE_PASSWORD 174 | image: sqshq/piggymetrics-turbine-stream-service 175 | restart: always 176 | depends_on: 177 | config: 178 | condition: service_healthy 179 | ports: 180 | - 8989:8989 181 | logging: 182 | options: 183 | max-size: "10m" 184 | max-file: "10" 185 | -------------------------------------------------------------------------------- /gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | MAINTAINER Alexander Lukyanchikov 3 | 4 | ADD ./target/gateway.jar /app/ 5 | CMD ["java", "-Xmx200m", "-jar", "/app/gateway.jar"] 6 | 7 | EXPOSE 4000 -------------------------------------------------------------------------------- /gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | gateway 7 | 1.0-SNAPSHOT 8 | jar 9 | 10 | gateway 11 | 12 | 13 | com.piggymetrics 14 | piggymetrics 15 | 1.0-SNAPSHOT 16 | 17 | 18 | 19 | 20 | org.springframework.cloud 21 | spring-cloud-starter-netflix-zuul 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-starter-config 26 | 27 | 28 | org.springframework.cloud 29 | spring-cloud-starter 30 | 31 | 32 | org.springframework.cloud 33 | spring-cloud-starter-netflix-eureka-client 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-starter-sleuth 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-starter-test 42 | test 43 | 44 | 45 | 46 | 47 | 48 | 49 | org.springframework.boot 50 | spring-boot-maven-plugin 51 | 52 | ${project.name} 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /gateway/src/main/java/com/piggymetrics/gateway/GatewayApplication.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.gateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.netflix.zuul.EnableZuulProxy; 7 | 8 | @SpringBootApplication 9 | @EnableDiscoveryClient 10 | @EnableZuulProxy 11 | public class GatewayApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(GatewayApplication.class, args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /gateway/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: gateway 4 | cloud: 5 | config: 6 | uri: http://config:8888 7 | fail-fast: true 8 | password: ${CONFIG_SERVICE_PASSWORD} 9 | username: user 10 | -------------------------------------------------------------------------------- /gateway/src/main/resources/static/attribution.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Piggy Metrics 8 | 9 | 10 | 11 | 12 |
13 |
14 | Creative Commons – Attribution (CC BY 3.0) 15 |
16 | Thanks a lot for icons from The Noun Project collection. 17 |
18 |
19 | Here's the list of all used icons: 20 |
21 | Piggy Bank designed by Jezmael Basilio 22 |
23 | Arrow designed by Jardson A. 24 |
25 | Wallet designed by Luis Prado 26 |
27 | Analytics designed by Aneeque Ahmed 28 |
29 | Piggy Bank designed by Michelle Ann 30 |
31 | Light Bulb designed by Chris Brunskill 32 |
33 | Speech Bubble designed by Cengiz SARI 34 |
35 | Bag designed by Agus Purwanto 36 |
37 | Analytics designed by Luboš Volkov 38 |
39 | College Tuition designed by Rediffusion 40 |
41 | Marijuana designed by Gareth 42 |
43 | Stroller designed by Edward Boatman 44 |
45 | Television designed by Piero Borgo 46 |
47 | Island designed by Bohdan Burmich 48 |
49 | Light Bulb designed by Rémy Médard 50 |
51 | Shirt designed by Megan Sheehan 52 |
53 | Telephone designed by Ian Mawle 54 |
55 | Shopping Cart designed by Megan Sheehan 56 |
57 | Gas designed by Jon Testa 58 |
59 | 60 | 61 | -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-100/museo-100.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/fonts/museo-100/museo-100.eot -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-100/museo-100.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/fonts/museo-100/museo-100.ttf -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-100/museo-100.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/fonts/museo-100/museo-100.woff -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-300/museo-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/fonts/museo-300/museo-300.eot -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-300/museo-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/fonts/museo-300/museo-300.ttf -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-300/museo-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/fonts/museo-300/museo-300.woff -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-500/museo-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/fonts/museo-500/museo-500.eot -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-500/museo-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/fonts/museo-500/museo-500.ttf -------------------------------------------------------------------------------- /gateway/src/main/resources/static/fonts/museo-500/museo-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/fonts/museo-500/museo-500.woff -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/1pagesprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/1pagesprites.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/1pagesprites@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/1pagesprites@2x.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/github.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/github.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/icons.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/icons@2x.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/linesbackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/linesbackground.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/linesbackground@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/linesbackground@2x.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/logo.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logo@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/logo@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logo_large.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/logo_large.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logo_large@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/logo_large@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logotext.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/logotext.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logotext@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/logotext@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logotext_large.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/logotext_large.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/logotext_large@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/logotext_large@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/overview.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/piggy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/piggy.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/piggy@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/piggy@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/piggy_large.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/piggy_large.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/piggy_large@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/piggy_large@2x.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/preloader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/preloader.gif -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/sprites.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/sprites@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/sprites@2x.png -------------------------------------------------------------------------------- /gateway/src/main/resources/static/images/userpic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sqshq/piggymetrics/6bb2cf9ddbca980b664d3edbb6ff775d75369278/gateway/src/main/resources/static/images/userpic.jpg -------------------------------------------------------------------------------- /gateway/src/main/resources/static/js/launch.js: -------------------------------------------------------------------------------- 1 | var global = { 2 | mobileClient: false, 3 | savePermit: true, 4 | usd: 0, 5 | eur: 0 6 | }; 7 | 8 | /** 9 | * Oauth2 10 | */ 11 | 12 | function requestOauthToken(username, password) { 13 | 14 | var success = false; 15 | 16 | $.ajax({ 17 | url: 'uaa/oauth/token', 18 | datatype: 'json', 19 | type: 'post', 20 | headers: {'Authorization': 'Basic YnJvd3Nlcjo='}, 21 | async: false, 22 | data: { 23 | scope: 'ui', 24 | username: username, 25 | password: password, 26 | grant_type: 'password' 27 | }, 28 | success: function (data) { 29 | localStorage.setItem('token', data.access_token); 30 | success = true; 31 | }, 32 | error: function () { 33 | removeOauthTokenFromStorage(); 34 | } 35 | }); 36 | 37 | return success; 38 | } 39 | 40 | function getOauthTokenFromStorage() { 41 | return localStorage.getItem('token'); 42 | } 43 | 44 | function removeOauthTokenFromStorage() { 45 | return localStorage.removeItem('token'); 46 | } 47 | 48 | /** 49 | * Current account 50 | */ 51 | 52 | function getCurrentAccount() { 53 | 54 | var token = getOauthTokenFromStorage(); 55 | var account = null; 56 | 57 | if (token) { 58 | $.ajax({ 59 | url: 'accounts/current', 60 | datatype: 'json', 61 | type: 'get', 62 | headers: {'Authorization': 'Bearer ' + token}, 63 | async: false, 64 | success: function (data) { 65 | account = data; 66 | }, 67 | error: function () { 68 | removeOauthTokenFromStorage(); 69 | } 70 | }); 71 | } 72 | 73 | return account; 74 | } 75 | 76 | $(window).load(function(){ 77 | 78 | if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) { 79 | FastClick.attach(document.body); 80 | global.mobileClient = true; 81 | } 82 | 83 | $.getJSON("https://api.exchangeratesapi.io/latest?base=RUB&symbols=EUR,USD", function( data ) { 84 | global.eur = 1 / data.rates.EUR; 85 | global.usd = 1 / data.rates.USD; 86 | }); 87 | 88 | var account = getCurrentAccount(); 89 | 90 | if (account) { 91 | showGreetingPage(account); 92 | } else { 93 | showLoginForm(); 94 | } 95 | }); 96 | 97 | function showGreetingPage(account) { 98 | initAccount(account); 99 | var userAvatar = $("").attr("src","images/userpic.jpg"); 100 | $(userAvatar).load(function() { 101 | setTimeout(initGreetingPage, 500); 102 | }); 103 | } 104 | 105 | function showLoginForm() { 106 | $("#loginpage").show(); 107 | $("#frontloginform").focus(); 108 | setTimeout(initialShaking, 700); 109 | } -------------------------------------------------------------------------------- /gateway/src/main/resources/static/js/login.js: -------------------------------------------------------------------------------- 1 | /** * Registration form */ $('#signup').submit(function(e) { e.preventDefault(); var username = $("input[id='backloginform']").val(); var password = $("input[id='backpasswordform']").val(); if (username.length < 3 || password.length < 6) { alert("Username must be at least 3 characters and password - at least 6. Be tricky!"); return; } if (username && password) { $.ajax({ url: 'accounts/', datatype: 'json', type: "post", contentType: "application/json", data: JSON.stringify({ username: username, password: password }), success: function (data) { requestOauthToken(username, password); initAccount(getCurrentAccount()); $('#registrationforms, .fliptext, #createaccount').fadeOut(300); $('#mailform').fadeIn(500); setTimeout(function(){ $("#backmailform").focus() }, 10); }, error: function (xhr, ajaxOptions, thrownError) { if (xhr.status == 400) { alert("Sorry, account with the same name already exists."); } else { alert("An error during account creation. Please, try again."); } } }); } else { alert("Please, fill all the fields."); } }); /** * E-mail form */ $('#mail').submit(function(e) { e.preventDefault(); var email = $("input[name='usermail']").val(); if (email) { $.ajax({ url: 'notifications/recipients/current', datatype: 'json', type: 'put', contentType: "application/json", data: JSON.stringify({ email: email, scheduledNotifications: { "REMIND": { "active": true, "frequency": "MONTHLY" } } }), headers: {'Authorization': 'Bearer ' + getOauthTokenFromStorage()}, async: true, success: function () { setTimeout(initGreetingPage, 200); setTimeout(function(){ $('#backmailform').val(''); $("#lastlogo").show(); }, 300); }, error: function (xhr, ajaxOptions, thrownError) { if (xhr.status == 400) { alert("Sorry, it seems your email address is invalid."); } else { alert("An error during saving notifications options"); } } }); } }); /** * Login */ function login() { $("#piggy").toggleClass("loadingspin"); $("#secondenter").hide(); $("#preloader, #lastlogo").show(); var username = $("input[id='frontloginform']").val(); var password = $("input[id='frontpasswordform']").val(); if (requestOauthToken(username, password)) { initAccount(getCurrentAccount()); var userAvatar = $("").attr("src","images/userpic.jpg"); $(userAvatar).load(function() { setTimeout(initGreetingPage, 500); }); } else { $("#preloader, #enter, #secondenter").hide(); flipForm(); $('.frontforms').val(''); $("#frontloginform").focus(); alert("Something went wrong. Please, check your credentials"); } } /** * Logout */ function logout() { removeOauthTokenFromStorage(); location.reload(); } /** * Demo */ $(".demobutton").bind("click", function(){ $.ajax({ url: 'accounts/demo', datatype: 'json', type: 'get', async: false, success: function (data) { global.savePermit = false; initAccount(data); var userAvatar = $("").attr("src","images/userpic.jpg"); $(userAvatar).load(function() { setTimeout(initGreetingPage, 500); }); }, error: function () { alert("Something went wrong. Please, try again"); } }); }); $("#skipmail").bind("click", function(){ $("#lastlogo").show(); setTimeout(initGreetingPage, 300); }); /** * Login form effects */ function initialShaking(){ autoShake(); setTimeout(autoShake, 1900); } function autoShake() { $("#piggy").toggleClass("auto-shake"); } function OnHoverShaking() { hoverShake(); setTimeout(hoverShake, 1700); } function hoverShake() { $("#piggy").toggleClass("hover-shake"); } function toggleInfo() { $("#infopage").toggle(); } function flipForm() { $("#cube").toggleClass("flippedform"); $("#frontpasswordform").focus(); } $("#piggy").on("click mouseover", function(){ if ($(this).hasClass("skakelogo") === false && $(this).hasClass("hover-shake") === false) { OnHoverShaking(); } }); $(".fliptext").bind("click", function(){ setTimeout( function() { $("#plusavatar").addClass("avataranimation"); } , 1000); $("#flipper").toggleClass("flippedcard"); }); $(".flipinfo").on("click", function() { $("#flipper").toggleClass("flippedcardinfo"); toggleInfo(); }); $(".frominfo, #infotitle, #infosubtitle").on("click", function() { $("#flipper").toggleClass("flippedcardinfo"); setTimeout(toggleInfo, 400); }); $("#enter").on("click", function() {flipForm()}); $("#secondenter").on("click", function() {login()}); $("#frontloginform").keyup(function (e) { if( $(this).val().length >= 3 ) { $("#enter").show(); if (e.which == 13) { flipForm(); $("#enter").hide(); } return; } else { $("#enter").hide(); } }); $("#frontpasswordform").keyup(function(e) { if ( $(this).val().length >= 6) { $("#secondenter").show(); if(e.which == 13) { $(this).blur(); login(); } return; } else { $("#secondenter").hide(); } }); -------------------------------------------------------------------------------- /gateway/src/test/java/com/piggymetrics/gateway/GatewayApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.gateway; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class GatewayApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | @Test 17 | public void fire() { 18 | 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /gateway/src/test/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | eureka: 2 | client: 3 | enabled: false -------------------------------------------------------------------------------- /mongodb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mongo:3 2 | MAINTAINER Alexander Lukyanchikov 3 | 4 | ADD init.sh /init.sh 5 | ADD ./dump / 6 | 7 | RUN \ 8 | chmod +x /init.sh && \ 9 | apt-get update && apt-get dist-upgrade -y --force-yes && apt-get install dos2unix && \ 10 | apt-get install psmisc -y -q && \ 11 | apt-get autoremove -y && apt-get clean && \ 12 | rm -rf /var/cache/* && rm -rf /var/lib/apt/lists/* && \ 13 | dos2unix -n /init.sh /initx.sh && chmod +x /initx.sh 14 | 15 | ENTRYPOINT ["/initx.sh"] -------------------------------------------------------------------------------- /mongodb/dump/account-service-dump.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates pre-filled demo account 3 | */ 4 | 5 | print('dump start'); 6 | 7 | db.accounts.update( 8 | { "_id": "demo" }, 9 | { 10 | "_id": "demo", 11 | "lastSeen": new Date(), 12 | "note": "demo note", 13 | "expenses": [ 14 | { 15 | "amount": 1300, 16 | "currency": "USD", 17 | "icon": "home", 18 | "period": "MONTH", 19 | "title": "Rent" 20 | }, 21 | { 22 | "amount": 120, 23 | "currency": "USD", 24 | "icon": "utilities", 25 | "period": "MONTH", 26 | "title": "Utilities" 27 | }, 28 | { 29 | "amount": 20, 30 | "currency": "USD", 31 | "icon": "meal", 32 | "period": "DAY", 33 | "title": "Meal" 34 | }, 35 | { 36 | "amount": 240, 37 | "currency": "USD", 38 | "icon": "gas", 39 | "period": "MONTH", 40 | "title": "Gas" 41 | }, 42 | { 43 | "amount": 3500, 44 | "currency": "EUR", 45 | "icon": "island", 46 | "period": "YEAR", 47 | "title": "Vacation" 48 | }, 49 | { 50 | "amount": 30, 51 | "currency": "EUR", 52 | "icon": "phone", 53 | "period": "MONTH", 54 | "title": "Phone" 55 | }, 56 | { 57 | "amount": 700, 58 | "currency": "USD", 59 | "icon": "sport", 60 | "period": "YEAR", 61 | "title": "Gym" 62 | } 63 | ], 64 | "incomes": [ 65 | { 66 | "amount": 42000, 67 | "currency": "USD", 68 | "icon": "wallet", 69 | "period": "YEAR", 70 | "title": "Salary" 71 | }, 72 | { 73 | "amount": 500, 74 | "currency": "USD", 75 | "icon": "edu", 76 | "period": "MONTH", 77 | "title": "Scholarship" 78 | } 79 | ], 80 | "saving": { 81 | "amount": 5900, 82 | "capitalization": false, 83 | "currency": "USD", 84 | "deposit": true, 85 | "interest": 3.32 86 | } 87 | }, 88 | { upsert: true } 89 | ); 90 | 91 | print('dump complete'); -------------------------------------------------------------------------------- /mongodb/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if test -z "$MONGODB_PASSWORD"; then 3 | echo "MONGODB_PASSWORD not defined" 4 | exit 1 5 | fi 6 | 7 | auth="-u user -p $MONGODB_PASSWORD" 8 | 9 | # MONGODB USER CREATION 10 | ( 11 | echo "setup mongodb auth" 12 | create_user="if (!db.getUser('user')) { db.createUser({ user: 'user', pwd: '$MONGODB_PASSWORD', roles: [ {role:'readWrite', db:'piggymetrics'} ]}) }" 13 | until mongo piggymetrics --eval "$create_user" || mongo piggymetrics $auth --eval "$create_user"; do sleep 5; done 14 | killall mongod 15 | sleep 1 16 | killall -9 mongod 17 | ) & 18 | 19 | # INIT DUMP EXECUTION 20 | ( 21 | if test -n "$INIT_DUMP"; then 22 | echo "execute dump file" 23 | until mongo piggymetrics $auth $INIT_DUMP; do sleep 5; done 24 | fi 25 | ) & 26 | 27 | echo "start mongodb without auth" 28 | chown -R mongodb /data/db 29 | gosu mongodb mongod "$@" 30 | 31 | echo "restarting with auth on" 32 | sleep 5 33 | exec gosu mongodb /usr/local/bin/docker-entrypoint.sh --auth "$@" 34 | -------------------------------------------------------------------------------- /monitoring/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | MAINTAINER Alexander Lukyanchikov 3 | 4 | ADD ./target/monitoring.jar /app/ 5 | CMD ["java", "-Xmx200m", "-jar", "/app/monitoring.jar"] 6 | 7 | EXPOSE 8080 -------------------------------------------------------------------------------- /monitoring/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | monitoring 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | monitoring 11 | 12 | 13 | com.piggymetrics 14 | piggymetrics 15 | 1.0-SNAPSHOT 16 | 17 | 18 | 19 | 20 | org.springframework.cloud 21 | spring-cloud-starter-config 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-starter-netflix-hystrix-dashboard 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-test 30 | test 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-maven-plugin 39 | 40 | ${project.name} 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /monitoring/src/main/java/com/piggymetrics/monitoring/MonitoringApplication.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.monitoring; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard; 6 | 7 | @SpringBootApplication 8 | @EnableHystrixDashboard 9 | public class MonitoringApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(MonitoringApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /monitoring/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: monitoring 4 | cloud: 5 | config: 6 | uri: http://config:8888 7 | fail-fast: true 8 | password: ${CONFIG_SERVICE_PASSWORD} 9 | username: user -------------------------------------------------------------------------------- /monitoring/src/test/java/com/piggymetrics/monitoring/MonitoringApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.monitoring; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class MonitoringApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /monitoring/src/test/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | eureka: 2 | client: 3 | enabled: false -------------------------------------------------------------------------------- /notification-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | MAINTAINER Alexander Lukyanchikov 3 | 4 | ADD ./target/notification-service.jar /app/ 5 | CMD ["java", "-Xmx200m", "-jar", "/app/notification-service.jar"] 6 | 7 | EXPOSE 8000 -------------------------------------------------------------------------------- /notification-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | notification-service 7 | 1.0.0-SNAPSHOT 8 | jar 9 | 10 | notification-service 11 | 12 | 13 | com.piggymetrics 14 | piggymetrics 15 | 1.0-SNAPSHOT 16 | 17 | 18 | 19 | 20 | org.springframework.cloud 21 | spring-cloud-starter-oauth2 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-security 26 | 27 | 28 | org.springframework.cloud 29 | spring-cloud-starter-config 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-starter-openfeign 38 | 39 | 40 | org.springframework.cloud 41 | spring-cloud-starter-netflix-eureka-client 42 | 43 | 44 | org.springframework.cloud 45 | spring-cloud-starter-sleuth 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-data-mongodb 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-actuator 54 | 55 | 56 | org.springframework.cloud 57 | spring-cloud-starter-bus-amqp 58 | 59 | 60 | org.springframework.cloud 61 | spring-cloud-netflix-hystrix-stream 62 | 63 | 64 | org.springframework.boot 65 | spring-boot-starter-mail 66 | 67 | 68 | 69 | de.flapdoodle.embed 70 | de.flapdoodle.embed.mongo 71 | 1.50.3 72 | test 73 | 74 | 75 | com.jayway.jsonpath 76 | json-path 77 | 2.2.0 78 | test 79 | 80 | 81 | org.springframework.boot 82 | spring-boot-starter-test 83 | test 84 | 85 | 86 | 87 | 88 | 89 | 90 | org.springframework.boot 91 | spring-boot-maven-plugin 92 | 93 | notification-service 94 | 95 | 96 | 97 | org.jacoco 98 | jacoco-maven-plugin 99 | 0.7.6.201602180812 100 | 101 | 102 | 103 | prepare-agent 104 | 105 | 106 | 107 | report 108 | test 109 | 110 | report 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/NotificationServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification; 2 | 3 | import com.piggymetrics.notification.repository.converter.FrequencyReaderConverter; 4 | import com.piggymetrics.notification.repository.converter.FrequencyWriterConverter; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 8 | import org.springframework.cloud.openfeign.EnableFeignClients; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.data.mongodb.core.convert.CustomConversions; 12 | import org.springframework.scheduling.annotation.EnableScheduling; 13 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 14 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; 15 | 16 | import java.util.Arrays; 17 | 18 | @SpringBootApplication 19 | @EnableDiscoveryClient 20 | @EnableOAuth2Client 21 | @EnableFeignClients 22 | @EnableGlobalMethodSecurity(prePostEnabled = true) 23 | @EnableScheduling 24 | public class NotificationServiceApplication { 25 | 26 | public static void main(String[] args) { 27 | SpringApplication.run(NotificationServiceApplication.class, args); 28 | } 29 | 30 | @Configuration 31 | static class CustomConversionsConfig { 32 | 33 | @Bean 34 | public CustomConversions customConversions() { 35 | return new CustomConversions(Arrays.asList(new FrequencyReaderConverter(), 36 | new FrequencyWriterConverter())); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/client/AccountServiceClient.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.client; 2 | 3 | import org.springframework.cloud.openfeign.FeignClient; 4 | import org.springframework.http.MediaType; 5 | import org.springframework.web.bind.annotation.PathVariable; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | 9 | @FeignClient(name = "account-service") 10 | public interface AccountServiceClient { 11 | 12 | @RequestMapping(method = RequestMethod.GET, value = "/accounts/{accountName}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE) 13 | String getAccount(@PathVariable("accountName") String accountName); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/config/ResourceServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.config; 2 | 3 | import feign.RequestInterceptor; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.cloud.security.oauth2.client.feign.OAuth2FeignRequestInterceptor; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext; 9 | import org.springframework.security.oauth2.client.OAuth2RestTemplate; 10 | import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsResourceDetails; 11 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 12 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 13 | 14 | /** 15 | * @author cdov 16 | */ 17 | @Configuration 18 | @EnableResourceServer 19 | public class ResourceServerConfig extends ResourceServerConfigurerAdapter { 20 | @Bean 21 | @ConfigurationProperties(prefix = "security.oauth2.client") 22 | public ClientCredentialsResourceDetails clientCredentialsResourceDetails() { 23 | return new ClientCredentialsResourceDetails(); 24 | } 25 | @Bean 26 | public RequestInterceptor oauth2FeignRequestInterceptor(){ 27 | return new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(), clientCredentialsResourceDetails()); 28 | } 29 | 30 | @Bean 31 | public OAuth2RestTemplate clientCredentialsRestTemplate() { 32 | return new OAuth2RestTemplate(clientCredentialsResourceDetails()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/controller/RecipientController.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.controller; 2 | 3 | import com.piggymetrics.notification.domain.Recipient; 4 | import com.piggymetrics.notification.service.RecipientService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.web.bind.annotation.RequestBody; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.RequestMethod; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import javax.validation.Valid; 12 | import java.security.Principal; 13 | 14 | @RestController 15 | @RequestMapping("/recipients") 16 | public class RecipientController { 17 | 18 | @Autowired 19 | private RecipientService recipientService; 20 | 21 | @RequestMapping(path = "/current", method = RequestMethod.GET) 22 | public Object getCurrentNotificationsSettings(Principal principal) { 23 | return recipientService.findByAccountName(principal.getName()); 24 | } 25 | 26 | @RequestMapping(path = "/current", method = RequestMethod.PUT) 27 | public Object saveCurrentNotificationsSettings(Principal principal, @Valid @RequestBody Recipient recipient) { 28 | return recipientService.save(principal.getName(), recipient); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/domain/Frequency.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.domain; 2 | 3 | import java.util.stream.Stream; 4 | 5 | public enum Frequency { 6 | 7 | WEEKLY(7), MONTHLY(30), QUARTERLY(90); 8 | 9 | private int days; 10 | 11 | Frequency(int days) { 12 | this.days = days; 13 | } 14 | 15 | public int getDays() { 16 | return days; 17 | } 18 | 19 | public static Frequency withDays(int days) { 20 | return Stream.of(Frequency.values()) 21 | .filter(f -> f.getDays() == days) 22 | .findFirst() 23 | .orElseThrow(IllegalArgumentException::new); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/domain/NotificationSettings.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.domain; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import java.util.Date; 5 | 6 | public class NotificationSettings { 7 | 8 | @NotNull 9 | private Boolean active; 10 | 11 | @NotNull 12 | private Frequency frequency; 13 | 14 | private Date lastNotified; 15 | 16 | public Boolean getActive() { 17 | return active; 18 | } 19 | 20 | public void setActive(Boolean active) { 21 | this.active = active; 22 | } 23 | 24 | public Frequency getFrequency() { 25 | return frequency; 26 | } 27 | 28 | public void setFrequency(Frequency frequency) { 29 | this.frequency = frequency; 30 | } 31 | 32 | public Date getLastNotified() { 33 | return lastNotified; 34 | } 35 | 36 | public void setLastNotified(Date lastNotified) { 37 | this.lastNotified = lastNotified; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/domain/NotificationType.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.domain; 2 | 3 | public enum NotificationType { 4 | 5 | BACKUP("backup.email.subject", "backup.email.text", "backup.email.attachment"), 6 | REMIND("remind.email.subject", "remind.email.text", null); 7 | 8 | private String subject; 9 | private String text; 10 | private String attachment; 11 | 12 | NotificationType(String subject, String text, String attachment) { 13 | this.subject = subject; 14 | this.text = text; 15 | this.attachment = attachment; 16 | } 17 | 18 | public String getSubject() { 19 | return subject; 20 | } 21 | 22 | public String getText() { 23 | return text; 24 | } 25 | 26 | public String getAttachment() { 27 | return attachment; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/domain/Recipient.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.domain; 2 | 3 | import org.hibernate.validator.constraints.Email; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | import javax.validation.Valid; 8 | import javax.validation.constraints.NotNull; 9 | import java.util.Map; 10 | 11 | @Document(collection = "recipients") 12 | public class Recipient { 13 | 14 | @Id 15 | private String accountName; 16 | 17 | @NotNull 18 | @Email 19 | private String email; 20 | 21 | @Valid 22 | private Map scheduledNotifications; 23 | 24 | public String getAccountName() { 25 | return accountName; 26 | } 27 | 28 | public void setAccountName(String accountName) { 29 | this.accountName = accountName; 30 | } 31 | 32 | public String getEmail() { 33 | return email; 34 | } 35 | 36 | public void setEmail(String email) { 37 | this.email = email; 38 | } 39 | 40 | public Map getScheduledNotifications() { 41 | return scheduledNotifications; 42 | } 43 | 44 | public void setScheduledNotifications(Map scheduledNotifications) { 45 | this.scheduledNotifications = scheduledNotifications; 46 | } 47 | 48 | @Override 49 | public String toString() { 50 | return "Recipient{" + 51 | "accountName='" + accountName + '\'' + 52 | ", email='" + email + '\'' + 53 | '}'; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/repository/RecipientRepository.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.repository; 2 | 3 | import com.piggymetrics.notification.domain.Recipient; 4 | import org.springframework.data.mongodb.repository.Query; 5 | import org.springframework.data.repository.CrudRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.List; 9 | 10 | @Repository 11 | public interface RecipientRepository extends CrudRepository { 12 | 13 | Recipient findByAccountName(String name); 14 | 15 | @Query("{ $and: [ {'scheduledNotifications.BACKUP.active': true }, { $where: 'this.scheduledNotifications.BACKUP.lastNotified < " + 16 | "new Date(new Date().setDate(new Date().getDate() - this.scheduledNotifications.BACKUP.frequency ))' }] }") 17 | List findReadyForBackup(); 18 | 19 | @Query("{ $and: [ {'scheduledNotifications.REMIND.active': true }, { $where: 'this.scheduledNotifications.REMIND.lastNotified < " + 20 | "new Date(new Date().setDate(new Date().getDate() - this.scheduledNotifications.REMIND.frequency ))' }] }") 21 | List findReadyForRemind(); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/repository/converter/FrequencyReaderConverter.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.repository.converter; 2 | 3 | import com.piggymetrics.notification.domain.Frequency; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class FrequencyReaderConverter implements Converter { 9 | 10 | @Override 11 | public Frequency convert(Integer days) { 12 | return Frequency.withDays(days); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/repository/converter/FrequencyWriterConverter.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.repository.converter; 2 | 3 | import com.piggymetrics.notification.domain.Frequency; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class FrequencyWriterConverter implements Converter { 9 | 10 | @Override 11 | public Integer convert(Frequency frequency) { 12 | return frequency.getDays(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/service/EmailService.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.service; 2 | 3 | import com.piggymetrics.notification.domain.NotificationType; 4 | import com.piggymetrics.notification.domain.Recipient; 5 | 6 | import javax.mail.MessagingException; 7 | import java.io.IOException; 8 | 9 | public interface EmailService { 10 | 11 | void send(NotificationType type, Recipient recipient, String attachment) throws MessagingException, IOException; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/service/EmailServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.service; 2 | 3 | import com.piggymetrics.notification.domain.NotificationType; 4 | import com.piggymetrics.notification.domain.Recipient; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.cloud.context.config.annotation.RefreshScope; 9 | import org.springframework.core.env.Environment; 10 | import org.springframework.core.io.ByteArrayResource; 11 | import org.springframework.mail.javamail.JavaMailSender; 12 | import org.springframework.mail.javamail.MimeMessageHelper; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.util.StringUtils; 15 | 16 | import javax.mail.MessagingException; 17 | import javax.mail.internet.MimeMessage; 18 | import java.io.IOException; 19 | import java.text.MessageFormat; 20 | 21 | @Service 22 | @RefreshScope 23 | public class EmailServiceImpl implements EmailService { 24 | 25 | private final Logger log = LoggerFactory.getLogger(getClass()); 26 | 27 | @Autowired 28 | private JavaMailSender mailSender; 29 | 30 | @Autowired 31 | private Environment env; 32 | 33 | @Override 34 | public void send(NotificationType type, Recipient recipient, String attachment) throws MessagingException, IOException { 35 | 36 | final String subject = env.getProperty(type.getSubject()); 37 | final String text = MessageFormat.format(env.getProperty(type.getText()), recipient.getAccountName()); 38 | 39 | MimeMessage message = mailSender.createMimeMessage(); 40 | 41 | MimeMessageHelper helper = new MimeMessageHelper(message, true); 42 | helper.setTo(recipient.getEmail()); 43 | helper.setSubject(subject); 44 | helper.setText(text); 45 | 46 | if (StringUtils.hasLength(attachment)) { 47 | helper.addAttachment(env.getProperty(type.getAttachment()), new ByteArrayResource(attachment.getBytes())); 48 | } 49 | 50 | mailSender.send(message); 51 | 52 | log.info("{} email notification has been send to {}", type, recipient.getEmail()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/service/NotificationService.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.service; 2 | 3 | public interface NotificationService { 4 | 5 | void sendBackupNotifications(); 6 | 7 | void sendRemindNotifications(); 8 | } 9 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/service/NotificationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.service; 2 | 3 | import com.piggymetrics.notification.client.AccountServiceClient; 4 | import com.piggymetrics.notification.domain.NotificationType; 5 | import com.piggymetrics.notification.domain.Recipient; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.scheduling.annotation.Scheduled; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.List; 13 | import java.util.concurrent.CompletableFuture; 14 | 15 | @Service 16 | public class NotificationServiceImpl implements NotificationService { 17 | 18 | private final Logger log = LoggerFactory.getLogger(getClass()); 19 | 20 | @Autowired 21 | private AccountServiceClient client; 22 | 23 | @Autowired 24 | private RecipientService recipientService; 25 | 26 | @Autowired 27 | private EmailService emailService; 28 | 29 | @Override 30 | @Scheduled(cron = "${backup.cron}") 31 | public void sendBackupNotifications() { 32 | 33 | final NotificationType type = NotificationType.BACKUP; 34 | 35 | List recipients = recipientService.findReadyToNotify(type); 36 | log.info("found {} recipients for backup notification", recipients.size()); 37 | 38 | recipients.forEach(recipient -> CompletableFuture.runAsync(() -> { 39 | try { 40 | String attachment = client.getAccount(recipient.getAccountName()); 41 | emailService.send(type, recipient, attachment); 42 | recipientService.markNotified(type, recipient); 43 | } catch (Throwable t) { 44 | log.error("an error during backup notification for {}", recipient, t); 45 | } 46 | })); 47 | } 48 | 49 | @Override 50 | @Scheduled(cron = "${remind.cron}") 51 | public void sendRemindNotifications() { 52 | 53 | final NotificationType type = NotificationType.REMIND; 54 | 55 | List recipients = recipientService.findReadyToNotify(type); 56 | log.info("found {} recipients for remind notification", recipients.size()); 57 | 58 | recipients.forEach(recipient -> CompletableFuture.runAsync(() -> { 59 | try { 60 | emailService.send(type, recipient, null); 61 | recipientService.markNotified(type, recipient); 62 | } catch (Throwable t) { 63 | log.error("an error during remind notification for {}", recipient, t); 64 | } 65 | })); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/service/RecipientService.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.service; 2 | 3 | import com.piggymetrics.notification.domain.NotificationType; 4 | import com.piggymetrics.notification.domain.Recipient; 5 | 6 | import java.util.List; 7 | 8 | public interface RecipientService { 9 | 10 | /** 11 | * Finds recipient by account name 12 | * 13 | * @param accountName 14 | * @return recipient 15 | */ 16 | Recipient findByAccountName(String accountName); 17 | 18 | /** 19 | * Finds recipients, which are ready to be notified 20 | * at the moment 21 | * 22 | * @param type 23 | * @return recipients to notify 24 | */ 25 | List findReadyToNotify(NotificationType type); 26 | 27 | /** 28 | * Creates or updates recipient settings 29 | * 30 | * @param accountName 31 | * @param recipient 32 | * @return updated recipient 33 | */ 34 | Recipient save(String accountName, Recipient recipient); 35 | 36 | /** 37 | * Updates {@link NotificationType} {@code lastNotified} property with current date 38 | * for given recipient. 39 | * 40 | * @param type 41 | * @param recipient 42 | */ 43 | void markNotified(NotificationType type, Recipient recipient); 44 | } 45 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/piggymetrics/notification/service/RecipientServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.service; 2 | 3 | import com.piggymetrics.notification.domain.NotificationType; 4 | import com.piggymetrics.notification.domain.Recipient; 5 | import com.piggymetrics.notification.repository.RecipientRepository; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.util.Assert; 11 | 12 | import java.util.Date; 13 | import java.util.List; 14 | 15 | @Service 16 | public class RecipientServiceImpl implements RecipientService { 17 | 18 | private final Logger log = LoggerFactory.getLogger(getClass()); 19 | 20 | @Autowired 21 | private RecipientRepository repository; 22 | 23 | @Override 24 | public Recipient findByAccountName(String accountName) { 25 | Assert.hasLength(accountName); 26 | return repository.findByAccountName(accountName); 27 | } 28 | 29 | /** 30 | * {@inheritDoc} 31 | */ 32 | @Override 33 | public Recipient save(String accountName, Recipient recipient) { 34 | 35 | recipient.setAccountName(accountName); 36 | recipient.getScheduledNotifications().values() 37 | .forEach(settings -> { 38 | if (settings.getLastNotified() == null) { 39 | settings.setLastNotified(new Date()); 40 | } 41 | }); 42 | 43 | repository.save(recipient); 44 | 45 | log.info("recipient {} settings has been updated", recipient); 46 | 47 | return recipient; 48 | } 49 | 50 | /** 51 | * {@inheritDoc} 52 | */ 53 | @Override 54 | public List findReadyToNotify(NotificationType type) { 55 | switch (type) { 56 | case BACKUP: 57 | return repository.findReadyForBackup(); 58 | case REMIND: 59 | return repository.findReadyForRemind(); 60 | default: 61 | throw new IllegalArgumentException(); 62 | } 63 | } 64 | 65 | /** 66 | * {@inheritDoc} 67 | */ 68 | @Override 69 | public void markNotified(NotificationType type, Recipient recipient) { 70 | recipient.getScheduledNotifications().get(type).setLastNotified(new Date()); 71 | repository.save(recipient); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /notification-service/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: notification-service 4 | cloud: 5 | config: 6 | uri: http://config:8888 7 | fail-fast: true 8 | password: ${CONFIG_SERVICE_PASSWORD} 9 | username: user -------------------------------------------------------------------------------- /notification-service/src/test/java/com/piggymetrics/notification/NotificationServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class NotificationServiceApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /notification-service/src/test/java/com/piggymetrics/notification/controller/RecipientControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.common.collect.ImmutableMap; 5 | import com.piggymetrics.notification.domain.Frequency; 6 | import com.piggymetrics.notification.domain.NotificationSettings; 7 | import com.piggymetrics.notification.domain.NotificationType; 8 | import com.piggymetrics.notification.domain.Recipient; 9 | import com.piggymetrics.notification.service.RecipientService; 10 | import com.sun.security.auth.UserPrincipal; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | import org.junit.runner.RunWith; 14 | import org.mockito.InjectMocks; 15 | import org.mockito.Mock; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.http.MediaType; 18 | import org.springframework.test.context.junit4.SpringRunner; 19 | import org.springframework.test.web.servlet.MockMvc; 20 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 21 | 22 | import static org.mockito.Mockito.when; 23 | import static org.mockito.MockitoAnnotations.initMocks; 24 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 25 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 27 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 28 | 29 | @RunWith(SpringRunner.class) 30 | @SpringBootTest 31 | public class RecipientControllerTest { 32 | 33 | private static final ObjectMapper mapper = new ObjectMapper(); 34 | 35 | @InjectMocks 36 | private RecipientController recipientController; 37 | 38 | @Mock 39 | private RecipientService recipientService; 40 | 41 | private MockMvc mockMvc; 42 | 43 | @Before 44 | public void setup() { 45 | initMocks(this); 46 | this.mockMvc = MockMvcBuilders.standaloneSetup(recipientController).build(); 47 | } 48 | 49 | @Test 50 | public void shouldSaveCurrentRecipientSettings() throws Exception { 51 | 52 | Recipient recipient = getStubRecipient(); 53 | String json = mapper.writeValueAsString(recipient); 54 | 55 | mockMvc.perform(put("/recipients/current").principal(new UserPrincipal(recipient.getAccountName())).contentType(MediaType.APPLICATION_JSON).content(json)) 56 | .andExpect(status().isOk()); 57 | } 58 | 59 | @Test 60 | public void shouldGetCurrentRecipientSettings() throws Exception { 61 | 62 | Recipient recipient = getStubRecipient(); 63 | when(recipientService.findByAccountName(recipient.getAccountName())).thenReturn(recipient); 64 | 65 | mockMvc.perform(get("/recipients/current").principal(new UserPrincipal(recipient.getAccountName()))) 66 | .andExpect(jsonPath("$.accountName").value(recipient.getAccountName())) 67 | .andExpect(status().isOk()); 68 | } 69 | 70 | private Recipient getStubRecipient() { 71 | 72 | NotificationSettings remind = new NotificationSettings(); 73 | remind.setActive(true); 74 | remind.setFrequency(Frequency.WEEKLY); 75 | remind.setLastNotified(null); 76 | 77 | NotificationSettings backup = new NotificationSettings(); 78 | backup.setActive(false); 79 | backup.setFrequency(Frequency.MONTHLY); 80 | backup.setLastNotified(null); 81 | 82 | Recipient recipient = new Recipient(); 83 | recipient.setAccountName("test"); 84 | recipient.setEmail("test@test.com"); 85 | recipient.setScheduledNotifications(ImmutableMap.of( 86 | NotificationType.BACKUP, backup, 87 | NotificationType.REMIND, remind 88 | )); 89 | 90 | return recipient; 91 | } 92 | } -------------------------------------------------------------------------------- /notification-service/src/test/java/com/piggymetrics/notification/service/EmailServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.service; 2 | 3 | import com.piggymetrics.notification.domain.NotificationType; 4 | import com.piggymetrics.notification.domain.Recipient; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.mockito.ArgumentCaptor; 8 | import org.mockito.Captor; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.springframework.core.env.Environment; 12 | import org.springframework.mail.javamail.JavaMailSender; 13 | 14 | import javax.mail.MessagingException; 15 | import javax.mail.Session; 16 | import javax.mail.internet.MimeMessage; 17 | import java.io.IOException; 18 | import java.util.Properties; 19 | 20 | import static org.junit.Assert.assertEquals; 21 | import static org.mockito.Mockito.verify; 22 | import static org.mockito.Mockito.when; 23 | import static org.mockito.MockitoAnnotations.initMocks; 24 | 25 | public class EmailServiceImplTest { 26 | 27 | @InjectMocks 28 | private EmailServiceImpl emailService; 29 | 30 | @Mock 31 | private JavaMailSender mailSender; 32 | 33 | @Mock 34 | private Environment env; 35 | 36 | @Captor 37 | private ArgumentCaptor captor; 38 | 39 | @Before 40 | public void setup() { 41 | initMocks(this); 42 | when(mailSender.createMimeMessage()) 43 | .thenReturn(new MimeMessage(Session.getDefaultInstance(new Properties()))); 44 | } 45 | 46 | @Test 47 | public void shouldSendBackupEmail() throws MessagingException, IOException { 48 | 49 | final String subject = "subject"; 50 | final String text = "text"; 51 | final String attachment = "attachment.json"; 52 | 53 | Recipient recipient = new Recipient(); 54 | recipient.setAccountName("test"); 55 | recipient.setEmail("test@test.com"); 56 | 57 | when(env.getProperty(NotificationType.BACKUP.getSubject())).thenReturn(subject); 58 | when(env.getProperty(NotificationType.BACKUP.getText())).thenReturn(text); 59 | when(env.getProperty(NotificationType.BACKUP.getAttachment())).thenReturn(attachment); 60 | 61 | emailService.send(NotificationType.BACKUP, recipient, "{\"name\":\"test\""); 62 | 63 | verify(mailSender).send(captor.capture()); 64 | 65 | MimeMessage message = captor.getValue(); 66 | assertEquals(subject, message.getSubject()); 67 | // TODO check other fields 68 | } 69 | 70 | @Test 71 | public void shouldSendRemindEmail() throws MessagingException, IOException { 72 | 73 | final String subject = "subject"; 74 | final String text = "text"; 75 | 76 | Recipient recipient = new Recipient(); 77 | recipient.setAccountName("test"); 78 | recipient.setEmail("test@test.com"); 79 | 80 | when(env.getProperty(NotificationType.REMIND.getSubject())).thenReturn(subject); 81 | when(env.getProperty(NotificationType.REMIND.getText())).thenReturn(text); 82 | 83 | emailService.send(NotificationType.REMIND, recipient, null); 84 | 85 | verify(mailSender).send(captor.capture()); 86 | 87 | MimeMessage message = captor.getValue(); 88 | assertEquals(subject, message.getSubject()); 89 | // TODO check other fields 90 | } 91 | } -------------------------------------------------------------------------------- /notification-service/src/test/java/com/piggymetrics/notification/service/NotificationServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.service; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.piggymetrics.notification.client.AccountServiceClient; 5 | import com.piggymetrics.notification.domain.NotificationType; 6 | import com.piggymetrics.notification.domain.Recipient; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | 12 | import javax.mail.MessagingException; 13 | import java.io.IOException; 14 | 15 | import static org.mockito.Mockito.*; 16 | import static org.mockito.MockitoAnnotations.initMocks; 17 | 18 | public class NotificationServiceImplTest { 19 | 20 | @InjectMocks 21 | private NotificationServiceImpl notificationService; 22 | 23 | @Mock 24 | private RecipientService recipientService; 25 | 26 | @Mock 27 | private AccountServiceClient client; 28 | 29 | @Mock 30 | private EmailService emailService; 31 | 32 | @Before 33 | public void setup() { 34 | initMocks(this); 35 | } 36 | 37 | @Test 38 | public void shouldSendBackupNotificationsEvenWhenErrorsOccursForSomeRecipients() throws IOException, MessagingException, InterruptedException { 39 | 40 | final String attachment = "json"; 41 | 42 | Recipient withError = new Recipient(); 43 | withError.setAccountName("with-error"); 44 | 45 | Recipient withNoError = new Recipient(); 46 | withNoError.setAccountName("with-no-error"); 47 | 48 | when(client.getAccount(withError.getAccountName())).thenThrow(new RuntimeException()); 49 | when(client.getAccount(withNoError.getAccountName())).thenReturn(attachment); 50 | 51 | when(recipientService.findReadyToNotify(NotificationType.BACKUP)).thenReturn(ImmutableList.of(withNoError, withError)); 52 | 53 | notificationService.sendBackupNotifications(); 54 | 55 | // TODO test concurrent code in a right way 56 | 57 | verify(emailService, timeout(100)).send(NotificationType.BACKUP, withNoError, attachment); 58 | verify(recipientService, timeout(100)).markNotified(NotificationType.BACKUP, withNoError); 59 | 60 | verify(recipientService, never()).markNotified(NotificationType.BACKUP, withError); 61 | } 62 | 63 | @Test 64 | public void shouldSendRemindNotificationsEvenWhenErrorsOccursForSomeRecipients() throws IOException, MessagingException, InterruptedException { 65 | 66 | final String attachment = "json"; 67 | 68 | Recipient withError = new Recipient(); 69 | withError.setAccountName("with-error"); 70 | 71 | Recipient withNoError = new Recipient(); 72 | withNoError.setAccountName("with-no-error"); 73 | 74 | when(recipientService.findReadyToNotify(NotificationType.REMIND)).thenReturn(ImmutableList.of(withNoError, withError)); 75 | doThrow(new RuntimeException()).when(emailService).send(NotificationType.REMIND, withError, null); 76 | 77 | notificationService.sendRemindNotifications(); 78 | 79 | // TODO test concurrent code in a right way 80 | 81 | verify(emailService, timeout(100)).send(NotificationType.REMIND, withNoError, null); 82 | verify(recipientService, timeout(100)).markNotified(NotificationType.REMIND, withNoError); 83 | 84 | verify(recipientService, never()).markNotified(NotificationType.REMIND, withError); 85 | } 86 | } -------------------------------------------------------------------------------- /notification-service/src/test/java/com/piggymetrics/notification/service/RecipientServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.notification.service; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.google.common.collect.ImmutableMap; 5 | import com.piggymetrics.notification.domain.Frequency; 6 | import com.piggymetrics.notification.domain.NotificationSettings; 7 | import com.piggymetrics.notification.domain.NotificationType; 8 | import com.piggymetrics.notification.domain.Recipient; 9 | import com.piggymetrics.notification.repository.RecipientRepository; 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.Mock; 14 | 15 | import java.util.Date; 16 | import java.util.List; 17 | 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertNotNull; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.when; 22 | import static org.mockito.MockitoAnnotations.initMocks; 23 | 24 | public class RecipientServiceImplTest { 25 | 26 | @InjectMocks 27 | private RecipientServiceImpl recipientService; 28 | 29 | @Mock 30 | private RecipientRepository repository; 31 | 32 | @Before 33 | public void setup() { 34 | initMocks(this); 35 | } 36 | 37 | @Test 38 | public void shouldFindByAccountName() { 39 | Recipient recipient = new Recipient(); 40 | recipient.setAccountName("test"); 41 | 42 | when(repository.findByAccountName(recipient.getAccountName())).thenReturn(recipient); 43 | Recipient found = recipientService.findByAccountName(recipient.getAccountName()); 44 | 45 | assertEquals(recipient, found); 46 | } 47 | 48 | @Test(expected = IllegalArgumentException.class) 49 | public void shouldFailToFindRecipientWhenAccountNameIsEmpty() { 50 | recipientService.findByAccountName(""); 51 | } 52 | 53 | @Test 54 | public void shouldSaveRecipient() { 55 | 56 | NotificationSettings remind = new NotificationSettings(); 57 | remind.setActive(true); 58 | remind.setFrequency(Frequency.WEEKLY); 59 | remind.setLastNotified(null); 60 | 61 | NotificationSettings backup = new NotificationSettings(); 62 | backup.setActive(false); 63 | backup.setFrequency(Frequency.MONTHLY); 64 | backup.setLastNotified(new Date()); 65 | 66 | Recipient recipient = new Recipient(); 67 | recipient.setEmail("test@test.com"); 68 | recipient.setScheduledNotifications(ImmutableMap.of( 69 | NotificationType.BACKUP, backup, 70 | NotificationType.REMIND, remind 71 | )); 72 | 73 | Recipient saved = recipientService.save("test", recipient); 74 | 75 | verify(repository).save(recipient); 76 | assertNotNull(saved.getScheduledNotifications().get(NotificationType.REMIND).getLastNotified()); 77 | assertEquals("test", saved.getAccountName()); 78 | } 79 | 80 | @Test 81 | public void shouldFindReadyToNotifyWhenNotificationTypeIsBackup() { 82 | final List recipients = ImmutableList.of(new Recipient()); 83 | when(repository.findReadyForBackup()).thenReturn(recipients); 84 | 85 | List found = recipientService.findReadyToNotify(NotificationType.BACKUP); 86 | assertEquals(recipients, found); 87 | } 88 | 89 | @Test 90 | public void shouldFindReadyToNotifyWhenNotificationTypeIsRemind() { 91 | final List recipients = ImmutableList.of(new Recipient()); 92 | when(repository.findReadyForRemind()).thenReturn(recipients); 93 | 94 | List found = recipientService.findReadyToNotify(NotificationType.REMIND); 95 | assertEquals(recipients, found); 96 | } 97 | 98 | @Test 99 | public void shouldMarkAsNotified() { 100 | 101 | NotificationSettings remind = new NotificationSettings(); 102 | remind.setActive(true); 103 | remind.setFrequency(Frequency.WEEKLY); 104 | remind.setLastNotified(null); 105 | 106 | Recipient recipient = new Recipient(); 107 | recipient.setAccountName("test"); 108 | recipient.setEmail("test@test.com"); 109 | recipient.setScheduledNotifications(ImmutableMap.of( 110 | NotificationType.REMIND, remind 111 | )); 112 | 113 | recipientService.markNotified(NotificationType.REMIND, recipient); 114 | assertNotNull(recipient.getScheduledNotifications().get(NotificationType.REMIND).getLastNotified()); 115 | verify(repository).save(recipient); 116 | } 117 | } -------------------------------------------------------------------------------- /notification-service/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | remind: 2 | cron: 0 0 0 * * * 3 | email: 4 | text: "Hey, {0}! We''ve missed you here on PiggyMetrics. It''s time to check your budget statistics.\r\n\r\nCheers,\r\nPiggyMetrics team" 5 | subject: PiggyMetrics reminder 6 | 7 | backup: 8 | cron: 0 0 12 * * * 9 | email: 10 | text: "Howdy, {0}. Your account backup is ready.\r\n\r\nCheers,\r\nPiggyMetrics team" 11 | subject: PiggyMetrics account backup 12 | attachment: backup.json 13 | 14 | spring: 15 | data: 16 | mongodb: 17 | database: piggymetrics 18 | port: 0 19 | mail: 20 | host: smtp.gmail.com 21 | port: 465 22 | username: test 23 | password: test 24 | properties: 25 | mail: 26 | smtp: 27 | auth: true 28 | socketFactory: 29 | port: 465 30 | class: javax.net.ssl.SSLSocketFactory 31 | fallback: false 32 | ssl: 33 | enable: true 34 | -------------------------------------------------------------------------------- /notification-service/src/test/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | eureka: 2 | client: 3 | enabled: false -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | com.piggymetrics 6 | piggymetrics 7 | 1.0-SNAPSHOT 8 | pom 9 | piggymetrics 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 2.0.3.RELEASE 15 | 16 | 17 | 18 | 19 | UTF-8 20 | Finchley.RELEASE 21 | 1.8 22 | 23 | 24 | 25 | 26 | 27 | org.springframework.cloud 28 | spring-cloud-dependencies 29 | ${spring-cloud.version} 30 | pom 31 | import 32 | 33 | 34 | 35 | 36 | 37 | config 38 | monitoring 39 | registry 40 | gateway 41 | auth-service 42 | account-service 43 | statistics-service 44 | notification-service 45 | turbine-stream-service 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /registry/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | MAINTAINER Alexander Lukyanchikov 3 | 4 | ADD ./target/registry.jar /app/ 5 | CMD ["java", "-Xmx200m", "-jar", "/app/registry.jar"] 6 | 7 | EXPOSE 8761 -------------------------------------------------------------------------------- /registry/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | registry 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | registry 11 | 12 | 13 | com.piggymetrics 14 | piggymetrics 15 | 1.0-SNAPSHOT 16 | 17 | 18 | 19 | 20 | org.springframework.cloud 21 | spring-cloud-starter-netflix-eureka-server 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-starter-config 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-test 30 | test 31 | 32 | 33 | 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-maven-plugin 39 | 40 | ${project.name} 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /registry/src/main/java/com/piggymetrics/registry/RegistryApplication.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.registry; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; 6 | 7 | @SpringBootApplication 8 | @EnableEurekaServer 9 | public class RegistryApplication { 10 | 11 | public static void main(String[] args) { 12 | SpringApplication.run(RegistryApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /registry/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: registry 4 | cloud: 5 | config: 6 | uri: http://config:8888 7 | fail-fast: true 8 | password: ${CONFIG_SERVICE_PASSWORD} 9 | username: user 10 | 11 | eureka: 12 | instance: 13 | prefer-ip-address: true 14 | client: 15 | registerWithEureka: false 16 | fetchRegistry: false 17 | server: 18 | waitTimeInMsWhenSyncEmpty: 0 -------------------------------------------------------------------------------- /statistics-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | MAINTAINER Alexander Lukyanchikov 3 | 4 | ADD ./target/statistics-service.jar /app/ 5 | CMD ["java", "-Xmx200m", "-jar", "/app/statistics-service.jar"] 6 | 7 | EXPOSE 7000 -------------------------------------------------------------------------------- /statistics-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | statistics-service 7 | 1.0-SNAPSHOT 8 | jar 9 | 10 | statistics-service 11 | 12 | 13 | com.piggymetrics 14 | piggymetrics 15 | 1.0-SNAPSHOT 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-starter-security 22 | 23 | 24 | org.springframework.cloud 25 | spring-cloud-starter-config 26 | 27 | 28 | org.springframework.cloud 29 | spring-cloud-starter-oauth2 30 | 31 | 32 | org.springframework.boot 33 | spring-boot-starter-web 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-starter-openfeign 38 | 39 | 40 | org.springframework.cloud 41 | spring-cloud-starter-netflix-eureka-client 42 | 43 | 44 | org.springframework.cloud 45 | spring-cloud-starter-sleuth 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-starter-data-mongodb 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-actuator 54 | 55 | 56 | org.springframework.cloud 57 | spring-cloud-starter-bus-amqp 58 | 59 | 60 | org.springframework.cloud 61 | spring-cloud-netflix-hystrix-stream 62 | 63 | 64 | com.google.guava 65 | guava 66 | 19.0 67 | 68 | 69 | 70 | org.springframework.boot 71 | spring-boot-starter-test 72 | test 73 | 74 | 75 | de.flapdoodle.embed 76 | de.flapdoodle.embed.mongo 77 | 1.50.3 78 | test 79 | 80 | 81 | com.jayway.jsonpath 82 | json-path 83 | 2.2.0 84 | test 85 | 86 | 87 | 88 | 89 | 90 | 91 | org.springframework.boot 92 | spring-boot-maven-plugin 93 | 94 | statistics-service 95 | 96 | 97 | 98 | org.jacoco 99 | jacoco-maven-plugin 100 | 0.7.6.201602180812 101 | 102 | 103 | 104 | prepare-agent 105 | 106 | 107 | 108 | report 109 | test 110 | 111 | report 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/StatisticsApplication.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics; 2 | 3 | import com.piggymetrics.statistics.repository.converter.DataPointIdReaderConverter; 4 | import com.piggymetrics.statistics.repository.converter.DataPointIdWriterConverter; 5 | import com.piggymetrics.statistics.service.security.CustomUserInfoTokenServices; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.SpringApplication; 8 | import org.springframework.boot.autoconfigure.SpringBootApplication; 9 | import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; 10 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 11 | import org.springframework.cloud.openfeign.EnableFeignClients; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.data.mongodb.core.convert.CustomConversions; 15 | import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 16 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; 17 | import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; 18 | 19 | import java.util.Arrays; 20 | 21 | @SpringBootApplication 22 | @EnableDiscoveryClient 23 | @EnableOAuth2Client 24 | @EnableFeignClients 25 | @EnableGlobalMethodSecurity(prePostEnabled = true) 26 | public class StatisticsApplication { 27 | 28 | public static void main(String[] args) { 29 | SpringApplication.run(StatisticsApplication.class, args); 30 | } 31 | 32 | @Configuration 33 | static class CustomConversionsConfig { 34 | 35 | @Bean 36 | public CustomConversions customConversions() { 37 | return new CustomConversions(Arrays.asList(new DataPointIdReaderConverter(), 38 | new DataPointIdWriterConverter())); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/client/ExchangeRatesClient.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.client; 2 | 3 | import com.piggymetrics.statistics.domain.Currency; 4 | import com.piggymetrics.statistics.domain.ExchangeRatesContainer; 5 | import org.springframework.cloud.openfeign.FeignClient; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.springframework.web.bind.annotation.RequestParam; 9 | 10 | @FeignClient(url = "${rates.url}", name = "rates-client", fallback = ExchangeRatesClientFallback.class) 11 | public interface ExchangeRatesClient { 12 | 13 | @RequestMapping(method = RequestMethod.GET, value = "/latest") 14 | ExchangeRatesContainer getRates(@RequestParam("base") Currency base); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/client/ExchangeRatesClientFallback.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.client; 2 | 3 | import com.piggymetrics.statistics.domain.Currency; 4 | import com.piggymetrics.statistics.domain.ExchangeRatesContainer; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.Collections; 8 | 9 | @Component 10 | public class ExchangeRatesClientFallback implements ExchangeRatesClient { 11 | 12 | @Override 13 | public ExchangeRatesContainer getRates(Currency base) { 14 | ExchangeRatesContainer container = new ExchangeRatesContainer(); 15 | container.setBase(Currency.getBase()); 16 | container.setRates(Collections.emptyMap()); 17 | return container; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/config/ResourceServerConfig.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.config; 2 | 3 | import com.piggymetrics.statistics.service.security.CustomUserInfoTokenServices; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; 9 | import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; 10 | import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices; 11 | 12 | /** 13 | * @author cdov 14 | */ 15 | @EnableResourceServer 16 | @Configuration 17 | public class ResourceServerConfig extends ResourceServerConfigurerAdapter { 18 | @Autowired 19 | private ResourceServerProperties sso; 20 | 21 | @Bean 22 | public ResourceServerTokenServices tokenServices() { 23 | return new CustomUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/controller/StatisticsController.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.controller; 2 | 3 | import com.piggymetrics.statistics.domain.Account; 4 | import com.piggymetrics.statistics.domain.timeseries.DataPoint; 5 | import com.piggymetrics.statistics.service.StatisticsService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.access.prepost.PreAuthorize; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import javax.validation.Valid; 11 | import java.security.Principal; 12 | import java.util.List; 13 | 14 | @RestController 15 | public class StatisticsController { 16 | 17 | @Autowired 18 | private StatisticsService statisticsService; 19 | 20 | @RequestMapping(value = "/current", method = RequestMethod.GET) 21 | public List getCurrentAccountStatistics(Principal principal) { 22 | return statisticsService.findByAccountName(principal.getName()); 23 | } 24 | 25 | @PreAuthorize("#oauth2.hasScope('server') or #accountName.equals('demo')") 26 | @RequestMapping(value = "/{accountName}", method = RequestMethod.GET) 27 | public List getStatisticsByAccountName(@PathVariable String accountName) { 28 | return statisticsService.findByAccountName(accountName); 29 | } 30 | 31 | @PreAuthorize("#oauth2.hasScope('server')") 32 | @RequestMapping(value = "/{accountName}", method = RequestMethod.PUT) 33 | public void saveAccountStatistics(@PathVariable String accountName, @Valid @RequestBody Account account) { 34 | statisticsService.save(accountName, account); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/domain/Account.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.domain; 2 | 3 | import org.codehaus.jackson.annotate.JsonIgnoreProperties; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | 6 | import javax.validation.Valid; 7 | import javax.validation.constraints.NotNull; 8 | import java.util.List; 9 | 10 | @Document(collection = "accounts") 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | public class Account { 13 | 14 | @Valid 15 | @NotNull 16 | private List incomes; 17 | 18 | @Valid 19 | @NotNull 20 | private List expenses; 21 | 22 | @Valid 23 | @NotNull 24 | private Saving saving; 25 | 26 | public List getIncomes() { 27 | return incomes; 28 | } 29 | 30 | public void setIncomes(List incomes) { 31 | this.incomes = incomes; 32 | } 33 | 34 | public List getExpenses() { 35 | return expenses; 36 | } 37 | 38 | public void setExpenses(List expenses) { 39 | this.expenses = expenses; 40 | } 41 | 42 | public Saving getSaving() { 43 | return saving; 44 | } 45 | 46 | public void setSaving(Saving saving) { 47 | this.saving = saving; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/domain/Currency.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.domain; 2 | 3 | public enum Currency { 4 | 5 | USD, EUR, RUB; 6 | 7 | public static Currency getBase() { 8 | return USD; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/domain/ExchangeRatesContainer.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | import java.math.BigDecimal; 6 | import java.time.LocalDate; 7 | import java.util.Map; 8 | 9 | @JsonIgnoreProperties(ignoreUnknown = true, value = {"date"}) 10 | public class ExchangeRatesContainer { 11 | 12 | private LocalDate date = LocalDate.now(); 13 | 14 | private Currency base; 15 | 16 | private Map rates; 17 | 18 | public LocalDate getDate() { 19 | return date; 20 | } 21 | 22 | public void setDate(LocalDate date) { 23 | this.date = date; 24 | } 25 | 26 | public Currency getBase() { 27 | return base; 28 | } 29 | 30 | public void setBase(Currency base) { 31 | this.base = base; 32 | } 33 | 34 | public Map getRates() { 35 | return rates; 36 | } 37 | 38 | public void setRates(Map rates) { 39 | this.rates = rates; 40 | } 41 | 42 | @Override 43 | public String toString() { 44 | return "RateList{" + 45 | "date=" + date + 46 | ", base=" + base + 47 | ", rates=" + rates + 48 | '}'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/domain/Item.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.domain; 2 | 3 | import org.hibernate.validator.constraints.Length; 4 | 5 | import javax.validation.constraints.NotNull; 6 | import java.math.BigDecimal; 7 | 8 | public class Item { 9 | 10 | @NotNull 11 | @Length(min = 1, max = 20) 12 | private String title; 13 | 14 | @NotNull 15 | private BigDecimal amount; 16 | 17 | @NotNull 18 | private Currency currency; 19 | 20 | @NotNull 21 | private TimePeriod period; 22 | 23 | public String getTitle() { 24 | return title; 25 | } 26 | 27 | public void setTitle(String title) { 28 | this.title = title; 29 | } 30 | 31 | public BigDecimal getAmount() { 32 | return amount; 33 | } 34 | 35 | public void setAmount(BigDecimal amount) { 36 | this.amount = amount; 37 | } 38 | 39 | public Currency getCurrency() { 40 | return currency; 41 | } 42 | 43 | public void setCurrency(Currency currency) { 44 | this.currency = currency; 45 | } 46 | 47 | public TimePeriod getPeriod() { 48 | return period; 49 | } 50 | 51 | public void setPeriod(TimePeriod period) { 52 | this.period = period; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/domain/Saving.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.domain; 2 | 3 | import javax.validation.constraints.NotNull; 4 | import java.math.BigDecimal; 5 | 6 | public class Saving { 7 | 8 | @NotNull 9 | private BigDecimal amount; 10 | 11 | @NotNull 12 | private Currency currency; 13 | 14 | @NotNull 15 | private BigDecimal interest; 16 | 17 | @NotNull 18 | private Boolean deposit; 19 | 20 | @NotNull 21 | private Boolean capitalization; 22 | 23 | public BigDecimal getAmount() { 24 | return amount; 25 | } 26 | 27 | public void setAmount(BigDecimal amount) { 28 | this.amount = amount; 29 | } 30 | 31 | public Currency getCurrency() { 32 | return currency; 33 | } 34 | 35 | public void setCurrency(Currency currency) { 36 | this.currency = currency; 37 | } 38 | 39 | public BigDecimal getInterest() { 40 | return interest; 41 | } 42 | 43 | public void setInterest(BigDecimal interest) { 44 | this.interest = interest; 45 | } 46 | 47 | public Boolean getDeposit() { 48 | return deposit; 49 | } 50 | 51 | public void setDeposit(Boolean deposit) { 52 | this.deposit = deposit; 53 | } 54 | 55 | public Boolean getCapitalization() { 56 | return capitalization; 57 | } 58 | 59 | public void setCapitalization(Boolean capitalization) { 60 | this.capitalization = capitalization; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/domain/TimePeriod.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.domain; 2 | 3 | import java.math.BigDecimal; 4 | 5 | public enum TimePeriod { 6 | 7 | YEAR(365.2425), QUARTER(91.3106), MONTH(30.4368), DAY(1), HOUR(0.0416); 8 | 9 | private double baseRatio; 10 | 11 | TimePeriod(double baseRatio) { 12 | this.baseRatio = baseRatio; 13 | } 14 | 15 | public BigDecimal getBaseRatio() { 16 | return new BigDecimal(baseRatio); 17 | } 18 | 19 | public static TimePeriod getBase() { 20 | return DAY; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/domain/timeseries/DataPoint.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.domain.timeseries; 2 | 3 | import com.piggymetrics.statistics.domain.Currency; 4 | import org.springframework.data.annotation.Id; 5 | import org.springframework.data.mongodb.core.mapping.Document; 6 | 7 | import java.math.BigDecimal; 8 | import java.util.Map; 9 | import java.util.Set; 10 | 11 | /** 12 | * Represents daily time series data point containing 13 | * current account state 14 | */ 15 | @Document(collection = "datapoints") 16 | public class DataPoint { 17 | 18 | @Id 19 | private DataPointId id; 20 | 21 | private Set incomes; 22 | 23 | private Set expenses; 24 | 25 | private Map statistics; 26 | 27 | private Map rates; 28 | 29 | public DataPointId getId() { 30 | return id; 31 | } 32 | 33 | public void setId(DataPointId id) { 34 | this.id = id; 35 | } 36 | 37 | public Set getIncomes() { 38 | return incomes; 39 | } 40 | 41 | public void setIncomes(Set incomes) { 42 | this.incomes = incomes; 43 | } 44 | 45 | public Set getExpenses() { 46 | return expenses; 47 | } 48 | 49 | public void setExpenses(Set expenses) { 50 | this.expenses = expenses; 51 | } 52 | 53 | public Map getStatistics() { 54 | return statistics; 55 | } 56 | 57 | public void setStatistics(Map statistics) { 58 | this.statistics = statistics; 59 | } 60 | 61 | public Map getRates() { 62 | return rates; 63 | } 64 | 65 | public void setRates(Map rates) { 66 | this.rates = rates; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/domain/timeseries/DataPointId.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.domain.timeseries; 2 | 3 | import java.io.Serializable; 4 | import java.util.Date; 5 | 6 | public class DataPointId implements Serializable { 7 | 8 | private static final long serialVersionUID = 1L; 9 | 10 | private String account; 11 | 12 | private Date date; 13 | 14 | public DataPointId(String account, Date date) { 15 | this.account = account; 16 | this.date = date; 17 | } 18 | 19 | public String getAccount() { 20 | return account; 21 | } 22 | 23 | public Date getDate() { 24 | return date; 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return "DataPointId{" + 30 | "account='" + account + '\'' + 31 | ", date=" + date + 32 | '}'; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/domain/timeseries/ItemMetric.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.domain.timeseries; 2 | 3 | import com.piggymetrics.statistics.domain.Currency; 4 | import com.piggymetrics.statistics.domain.TimePeriod; 5 | 6 | import java.math.BigDecimal; 7 | 8 | /** 9 | * Represents normalized {@link com.piggymetrics.statistics.domain.Item} object 10 | * with {@link Currency#getBase()} currency and {@link TimePeriod#getBase()} time period 11 | */ 12 | public class ItemMetric { 13 | 14 | private String title; 15 | 16 | private BigDecimal amount; 17 | 18 | public ItemMetric(String title, BigDecimal amount) { 19 | this.title = title; 20 | this.amount = amount; 21 | } 22 | 23 | public String getTitle() { 24 | return title; 25 | } 26 | 27 | public BigDecimal getAmount() { 28 | return amount; 29 | } 30 | 31 | @Override 32 | public boolean equals(Object o) { 33 | if (this == o) return true; 34 | if (o == null || getClass() != o.getClass()) return false; 35 | 36 | ItemMetric that = (ItemMetric) o; 37 | 38 | return title.equalsIgnoreCase(that.title); 39 | 40 | } 41 | 42 | @Override 43 | public int hashCode() { 44 | return title.hashCode(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/domain/timeseries/StatisticMetric.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.domain.timeseries; 2 | 3 | public enum StatisticMetric { 4 | 5 | INCOMES_AMOUNT, EXPENSES_AMOUNT, SAVING_AMOUNT 6 | 7 | } 8 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/repository/DataPointRepository.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.repository; 2 | 3 | import com.piggymetrics.statistics.domain.timeseries.DataPoint; 4 | import com.piggymetrics.statistics.domain.timeseries.DataPointId; 5 | import org.springframework.data.repository.CrudRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.List; 9 | 10 | @Repository 11 | public interface DataPointRepository extends CrudRepository { 12 | 13 | List findByIdAccount(String account); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/repository/converter/DataPointIdReaderConverter.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.repository.converter; 2 | 3 | import com.mongodb.DBObject; 4 | import com.piggymetrics.statistics.domain.timeseries.DataPointId; 5 | import org.springframework.core.convert.converter.Converter; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.util.Date; 9 | 10 | @Component 11 | public class DataPointIdReaderConverter implements Converter { 12 | 13 | @Override 14 | public DataPointId convert(DBObject object) { 15 | 16 | Date date = (Date) object.get("date"); 17 | String account = (String) object.get("account"); 18 | 19 | return new DataPointId(account, date); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/repository/converter/DataPointIdWriterConverter.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.repository.converter; 2 | 3 | import com.mongodb.BasicDBObject; 4 | import com.mongodb.DBObject; 5 | import com.piggymetrics.statistics.domain.timeseries.DataPointId; 6 | import org.springframework.core.convert.converter.Converter; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | public class DataPointIdWriterConverter implements Converter { 11 | 12 | private static final int FIELDS = 2; 13 | 14 | @Override 15 | public DBObject convert(DataPointId id) { 16 | 17 | DBObject object = new BasicDBObject(FIELDS); 18 | 19 | object.put("date", id.getDate()); 20 | object.put("account", id.getAccount()); 21 | 22 | return object; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/service/ExchangeRatesService.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.service; 2 | 3 | import com.piggymetrics.statistics.domain.Currency; 4 | 5 | import java.math.BigDecimal; 6 | import java.util.Map; 7 | 8 | public interface ExchangeRatesService { 9 | 10 | /** 11 | * Requests today's foreign exchange rates from a provider 12 | * or reuses values from the last request (if they are still relevant) 13 | * 14 | * @return current date rates 15 | */ 16 | Map getCurrentRates(); 17 | 18 | /** 19 | * Converts given amount to specified currency 20 | * 21 | * @param from {@link Currency} 22 | * @param to {@link Currency} 23 | * @param amount to be converted 24 | * @return converted amount 25 | */ 26 | BigDecimal convert(Currency from, Currency to, BigDecimal amount); 27 | } 28 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/service/ExchangeRatesServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.service; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.piggymetrics.statistics.client.ExchangeRatesClient; 5 | import com.piggymetrics.statistics.domain.Currency; 6 | import com.piggymetrics.statistics.domain.ExchangeRatesContainer; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.util.Assert; 12 | 13 | import java.math.BigDecimal; 14 | import java.math.RoundingMode; 15 | import java.time.LocalDate; 16 | import java.util.Map; 17 | 18 | @Service 19 | public class ExchangeRatesServiceImpl implements ExchangeRatesService { 20 | 21 | private static final Logger log = LoggerFactory.getLogger(ExchangeRatesServiceImpl.class); 22 | 23 | private ExchangeRatesContainer container; 24 | 25 | @Autowired 26 | private ExchangeRatesClient client; 27 | 28 | /** 29 | * {@inheritDoc} 30 | */ 31 | @Override 32 | public Map getCurrentRates() { 33 | 34 | if (container == null || !container.getDate().equals(LocalDate.now())) { 35 | container = client.getRates(Currency.getBase()); 36 | log.info("exchange rates has been updated: {}", container); 37 | } 38 | 39 | return ImmutableMap.of( 40 | Currency.EUR, container.getRates().get(Currency.EUR.name()), 41 | Currency.RUB, container.getRates().get(Currency.RUB.name()), 42 | Currency.USD, BigDecimal.ONE 43 | ); 44 | } 45 | 46 | /** 47 | * {@inheritDoc} 48 | */ 49 | @Override 50 | public BigDecimal convert(Currency from, Currency to, BigDecimal amount) { 51 | 52 | Assert.notNull(amount); 53 | 54 | Map rates = getCurrentRates(); 55 | BigDecimal ratio = rates.get(to).divide(rates.get(from), 4, RoundingMode.HALF_UP); 56 | 57 | return amount.multiply(ratio); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/service/StatisticsService.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.service; 2 | 3 | import com.piggymetrics.statistics.domain.Account; 4 | import com.piggymetrics.statistics.domain.timeseries.DataPoint; 5 | 6 | import java.util.List; 7 | 8 | public interface StatisticsService { 9 | 10 | /** 11 | * Finds account by given name 12 | * 13 | * @param accountName 14 | * @return found account 15 | */ 16 | List findByAccountName(String accountName); 17 | 18 | /** 19 | * Converts given {@link Account} object to {@link DataPoint} with 20 | * a set of significant statistic metrics. 21 | * 22 | * Compound {@link DataPoint#id} forces to rewrite the object 23 | * for each account within a day. 24 | * 25 | * @param accountName 26 | * @param account 27 | */ 28 | DataPoint save(String accountName, Account account); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/piggymetrics/statistics/service/StatisticsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.service; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.piggymetrics.statistics.domain.*; 5 | import com.piggymetrics.statistics.domain.timeseries.DataPoint; 6 | import com.piggymetrics.statistics.domain.timeseries.DataPointId; 7 | import com.piggymetrics.statistics.domain.timeseries.ItemMetric; 8 | import com.piggymetrics.statistics.domain.timeseries.StatisticMetric; 9 | import com.piggymetrics.statistics.repository.DataPointRepository; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.stereotype.Service; 14 | import org.springframework.util.Assert; 15 | 16 | import java.math.BigDecimal; 17 | import java.math.RoundingMode; 18 | import java.time.Instant; 19 | import java.time.LocalDate; 20 | import java.time.ZoneId; 21 | import java.util.Date; 22 | import java.util.List; 23 | import java.util.Map; 24 | import java.util.Set; 25 | import java.util.stream.Collectors; 26 | 27 | @Service 28 | public class StatisticsServiceImpl implements StatisticsService { 29 | 30 | private final Logger log = LoggerFactory.getLogger(getClass()); 31 | 32 | @Autowired 33 | private DataPointRepository repository; 34 | 35 | @Autowired 36 | private ExchangeRatesService ratesService; 37 | 38 | /** 39 | * {@inheritDoc} 40 | */ 41 | @Override 42 | public List findByAccountName(String accountName) { 43 | Assert.hasLength(accountName); 44 | return repository.findByIdAccount(accountName); 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | @Override 51 | public DataPoint save(String accountName, Account account) { 52 | 53 | Instant instant = LocalDate.now().atStartOfDay() 54 | .atZone(ZoneId.systemDefault()).toInstant(); 55 | 56 | DataPointId pointId = new DataPointId(accountName, Date.from(instant)); 57 | 58 | Set incomes = account.getIncomes().stream() 59 | .map(this::createItemMetric) 60 | .collect(Collectors.toSet()); 61 | 62 | Set expenses = account.getExpenses().stream() 63 | .map(this::createItemMetric) 64 | .collect(Collectors.toSet()); 65 | 66 | Map statistics = createStatisticMetrics(incomes, expenses, account.getSaving()); 67 | 68 | DataPoint dataPoint = new DataPoint(); 69 | dataPoint.setId(pointId); 70 | dataPoint.setIncomes(incomes); 71 | dataPoint.setExpenses(expenses); 72 | dataPoint.setStatistics(statistics); 73 | dataPoint.setRates(ratesService.getCurrentRates()); 74 | 75 | log.debug("new datapoint has been created: {}", pointId); 76 | 77 | return repository.save(dataPoint); 78 | } 79 | 80 | private Map createStatisticMetrics(Set incomes, Set expenses, Saving saving) { 81 | 82 | BigDecimal savingAmount = ratesService.convert(saving.getCurrency(), Currency.getBase(), saving.getAmount()); 83 | 84 | BigDecimal expensesAmount = expenses.stream() 85 | .map(ItemMetric::getAmount) 86 | .reduce(BigDecimal.ZERO, BigDecimal::add); 87 | 88 | BigDecimal incomesAmount = incomes.stream() 89 | .map(ItemMetric::getAmount) 90 | .reduce(BigDecimal.ZERO, BigDecimal::add); 91 | 92 | return ImmutableMap.of( 93 | StatisticMetric.EXPENSES_AMOUNT, expensesAmount, 94 | StatisticMetric.INCOMES_AMOUNT, incomesAmount, 95 | StatisticMetric.SAVING_AMOUNT, savingAmount 96 | ); 97 | } 98 | 99 | /** 100 | * Normalizes given item amount to {@link Currency#getBase()} currency with 101 | * {@link TimePeriod#getBase()} time period 102 | */ 103 | private ItemMetric createItemMetric(Item item) { 104 | 105 | BigDecimal amount = ratesService 106 | .convert(item.getCurrency(), Currency.getBase(), item.getAmount()) 107 | .divide(item.getPeriod().getBaseRatio(), 4, RoundingMode.HALF_UP); 108 | 109 | return new ItemMetric(item.getTitle(), amount); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /statistics-service/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: statistics-service 4 | cloud: 5 | config: 6 | uri: http://config:8888 7 | fail-fast: true 8 | password: ${CONFIG_SERVICE_PASSWORD} 9 | username: user -------------------------------------------------------------------------------- /statistics-service/src/test/java/com/piggymetrics/statistics/StatisticsServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class StatisticsServiceApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /statistics-service/src/test/java/com/piggymetrics/statistics/client/ExchangeRatesClientTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.client; 2 | 3 | import com.piggymetrics.statistics.domain.Currency; 4 | import com.piggymetrics.statistics.domain.ExchangeRatesContainer; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.test.context.junit4.SpringRunner; 10 | 11 | import java.time.LocalDate; 12 | 13 | import static org.junit.Assert.assertEquals; 14 | import static org.junit.Assert.assertNotNull; 15 | 16 | @RunWith(SpringRunner.class) 17 | @SpringBootTest 18 | public class ExchangeRatesClientTest { 19 | 20 | @Autowired 21 | private ExchangeRatesClient client; 22 | 23 | @Test 24 | public void shouldRetrieveExchangeRates() { 25 | 26 | ExchangeRatesContainer container = client.getRates(Currency.getBase()); 27 | 28 | assertEquals(container.getDate(), LocalDate.now()); 29 | assertEquals(container.getBase(), Currency.getBase()); 30 | 31 | assertNotNull(container.getRates()); 32 | assertNotNull(container.getRates().get(Currency.USD.name())); 33 | assertNotNull(container.getRates().get(Currency.EUR.name())); 34 | assertNotNull(container.getRates().get(Currency.RUB.name())); 35 | } 36 | 37 | @Test 38 | public void shouldRetrieveExchangeRatesForSpecifiedCurrency() { 39 | 40 | Currency requestedCurrency = Currency.EUR; 41 | ExchangeRatesContainer container = client.getRates(Currency.getBase()); 42 | 43 | assertEquals(container.getDate(), LocalDate.now()); 44 | assertEquals(container.getBase(), Currency.getBase()); 45 | 46 | assertNotNull(container.getRates()); 47 | assertNotNull(container.getRates().get(requestedCurrency.name())); 48 | } 49 | } -------------------------------------------------------------------------------- /statistics-service/src/test/java/com/piggymetrics/statistics/controller/StatisticsControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.common.collect.ImmutableList; 5 | import com.piggymetrics.statistics.domain.Account; 6 | import com.piggymetrics.statistics.domain.Currency; 7 | import com.piggymetrics.statistics.domain.Item; 8 | import com.piggymetrics.statistics.domain.Saving; 9 | import com.piggymetrics.statistics.domain.TimePeriod; 10 | import com.piggymetrics.statistics.domain.timeseries.DataPoint; 11 | import com.piggymetrics.statistics.domain.timeseries.DataPointId; 12 | import com.piggymetrics.statistics.service.StatisticsService; 13 | import com.sun.security.auth.UserPrincipal; 14 | import org.junit.Before; 15 | import org.junit.Test; 16 | import org.junit.runner.RunWith; 17 | import org.mockito.InjectMocks; 18 | import org.mockito.Mock; 19 | import org.springframework.boot.test.context.SpringBootTest; 20 | import org.springframework.http.MediaType; 21 | import org.springframework.test.context.junit4.SpringRunner; 22 | import org.springframework.test.web.servlet.MockMvc; 23 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 24 | 25 | import java.math.BigDecimal; 26 | import java.util.Date; 27 | 28 | import static org.mockito.ArgumentMatchers.any; 29 | import static org.mockito.ArgumentMatchers.anyString; 30 | import static org.mockito.Mockito.verify; 31 | import static org.mockito.Mockito.when; 32 | import static org.mockito.MockitoAnnotations.initMocks; 33 | import static org.mockito.internal.verification.VerificationModeFactory.times; 34 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 35 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; 36 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 37 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 38 | 39 | @RunWith(SpringRunner.class) 40 | @SpringBootTest 41 | public class StatisticsControllerTest { 42 | 43 | private static final ObjectMapper mapper = new ObjectMapper(); 44 | 45 | @InjectMocks 46 | private StatisticsController statisticsController; 47 | 48 | @Mock 49 | private StatisticsService statisticsService; 50 | 51 | private MockMvc mockMvc; 52 | 53 | @Before 54 | public void setup() { 55 | initMocks(this); 56 | this.mockMvc = MockMvcBuilders.standaloneSetup(statisticsController).build(); 57 | } 58 | 59 | @Test 60 | public void shouldGetStatisticsByAccountName() throws Exception { 61 | 62 | final DataPoint dataPoint = new DataPoint(); 63 | dataPoint.setId(new DataPointId("test", new Date())); 64 | 65 | when(statisticsService.findByAccountName(dataPoint.getId().getAccount())) 66 | .thenReturn(ImmutableList.of(dataPoint)); 67 | 68 | mockMvc.perform(get("/test").principal(new UserPrincipal(dataPoint.getId().getAccount()))) 69 | .andExpect(jsonPath("$[0].id.account").value(dataPoint.getId().getAccount())) 70 | .andExpect(status().isOk()); 71 | } 72 | 73 | @Test 74 | public void shouldGetCurrentAccountStatistics() throws Exception { 75 | 76 | final DataPoint dataPoint = new DataPoint(); 77 | dataPoint.setId(new DataPointId("test", new Date())); 78 | 79 | when(statisticsService.findByAccountName(dataPoint.getId().getAccount())) 80 | .thenReturn(ImmutableList.of(dataPoint)); 81 | 82 | mockMvc.perform(get("/current").principal(new UserPrincipal(dataPoint.getId().getAccount()))) 83 | .andExpect(jsonPath("$[0].id.account").value(dataPoint.getId().getAccount())) 84 | .andExpect(status().isOk()); 85 | } 86 | 87 | @Test 88 | public void shouldSaveAccountStatistics() throws Exception { 89 | 90 | Saving saving = new Saving(); 91 | saving.setAmount(new BigDecimal(1500)); 92 | saving.setCurrency(Currency.USD); 93 | saving.setInterest(new BigDecimal("3.32")); 94 | saving.setDeposit(true); 95 | saving.setCapitalization(false); 96 | 97 | Item grocery = new Item(); 98 | grocery.setTitle("Grocery"); 99 | grocery.setAmount(new BigDecimal(10)); 100 | grocery.setCurrency(Currency.USD); 101 | grocery.setPeriod(TimePeriod.DAY); 102 | 103 | Item salary = new Item(); 104 | salary.setTitle("Salary"); 105 | salary.setAmount(new BigDecimal(9100)); 106 | salary.setCurrency(Currency.USD); 107 | salary.setPeriod(TimePeriod.MONTH); 108 | 109 | final Account account = new Account(); 110 | account.setSaving(saving); 111 | account.setExpenses(ImmutableList.of(grocery)); 112 | account.setIncomes(ImmutableList.of(salary)); 113 | 114 | String json = mapper.writeValueAsString(account); 115 | 116 | mockMvc.perform(put("/test").contentType(MediaType.APPLICATION_JSON).content(json)) 117 | .andExpect(status().isOk()); 118 | 119 | verify(statisticsService, times(1)).save(anyString(), any(Account.class)); 120 | } 121 | } -------------------------------------------------------------------------------- /statistics-service/src/test/java/com/piggymetrics/statistics/repository/DataPointRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.repository; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.google.common.collect.Sets; 5 | import com.piggymetrics.statistics.domain.timeseries.DataPoint; 6 | import com.piggymetrics.statistics.domain.timeseries.DataPointId; 7 | import com.piggymetrics.statistics.domain.timeseries.ItemMetric; 8 | import com.piggymetrics.statistics.domain.timeseries.StatisticMetric; 9 | import org.junit.Test; 10 | import org.junit.runner.RunWith; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest; 13 | import org.springframework.test.context.junit4.SpringRunner; 14 | 15 | import java.math.BigDecimal; 16 | import java.util.Date; 17 | import java.util.List; 18 | 19 | import static org.junit.Assert.assertEquals; 20 | 21 | @RunWith(SpringRunner.class) 22 | @DataMongoTest 23 | public class DataPointRepositoryTest { 24 | 25 | @Autowired 26 | private DataPointRepository repository; 27 | 28 | @Test 29 | public void shouldSaveDataPoint() { 30 | 31 | ItemMetric salary = new ItemMetric("salary", new BigDecimal(20_000)); 32 | 33 | ItemMetric grocery = new ItemMetric("grocery", new BigDecimal(1_000)); 34 | ItemMetric vacation = new ItemMetric("vacation", new BigDecimal(2_000)); 35 | 36 | DataPointId pointId = new DataPointId("test-account", new Date(0)); 37 | 38 | DataPoint point = new DataPoint(); 39 | point.setId(pointId); 40 | point.setIncomes(Sets.newHashSet(salary)); 41 | point.setExpenses(Sets.newHashSet(grocery, vacation)); 42 | point.setStatistics(ImmutableMap.of( 43 | StatisticMetric.SAVING_AMOUNT, new BigDecimal(400_000), 44 | StatisticMetric.INCOMES_AMOUNT, new BigDecimal(20_000), 45 | StatisticMetric.EXPENSES_AMOUNT, new BigDecimal(3_000) 46 | )); 47 | 48 | repository.save(point); 49 | 50 | List points = repository.findByIdAccount(pointId.getAccount()); 51 | assertEquals(1, points.size()); 52 | assertEquals(pointId.getDate(), points.get(0).getId().getDate()); 53 | assertEquals(point.getStatistics().size(), points.get(0).getStatistics().size()); 54 | assertEquals(point.getIncomes().size(), points.get(0).getIncomes().size()); 55 | assertEquals(point.getExpenses().size(), points.get(0).getExpenses().size()); 56 | } 57 | 58 | @Test 59 | public void shouldRewriteDataPointWithinADay() { 60 | 61 | final BigDecimal earlyAmount = new BigDecimal(100); 62 | final BigDecimal lateAmount = new BigDecimal(200); 63 | 64 | DataPointId pointId = new DataPointId("test-account", new Date(0)); 65 | 66 | DataPoint earlier = new DataPoint(); 67 | earlier.setId(pointId); 68 | earlier.setStatistics(ImmutableMap.of( 69 | StatisticMetric.SAVING_AMOUNT, earlyAmount 70 | )); 71 | 72 | repository.save(earlier); 73 | 74 | DataPoint later = new DataPoint(); 75 | later.setId(pointId); 76 | later.setStatistics(ImmutableMap.of( 77 | StatisticMetric.SAVING_AMOUNT, lateAmount 78 | )); 79 | 80 | repository.save(later); 81 | 82 | List points = repository.findByIdAccount(pointId.getAccount()); 83 | 84 | assertEquals(1, points.size()); 85 | assertEquals(lateAmount, points.get(0).getStatistics().get(StatisticMetric.SAVING_AMOUNT)); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /statistics-service/src/test/java/com/piggymetrics/statistics/service/ExchangeRatesServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.statistics.service; 2 | 3 | import com.google.common.collect.ImmutableMap; 4 | import com.piggymetrics.statistics.client.ExchangeRatesClient; 5 | import com.piggymetrics.statistics.domain.Currency; 6 | import com.piggymetrics.statistics.domain.ExchangeRatesContainer; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | 12 | import java.math.BigDecimal; 13 | import java.util.Map; 14 | 15 | import static org.junit.Assert.assertEquals; 16 | import static org.junit.Assert.assertTrue; 17 | import static org.mockito.Mockito.*; 18 | import static org.mockito.MockitoAnnotations.initMocks; 19 | 20 | public class ExchangeRatesServiceImplTest { 21 | 22 | @InjectMocks 23 | private ExchangeRatesServiceImpl ratesService; 24 | 25 | @Mock 26 | private ExchangeRatesClient client; 27 | 28 | @Before 29 | public void setup() { 30 | initMocks(this); 31 | } 32 | 33 | @Test 34 | public void shouldReturnCurrentRatesWhenContainerIsEmptySoFar() { 35 | 36 | ExchangeRatesContainer container = new ExchangeRatesContainer(); 37 | container.setRates(ImmutableMap.of( 38 | Currency.EUR.name(), new BigDecimal("0.8"), 39 | Currency.RUB.name(), new BigDecimal("80") 40 | )); 41 | 42 | when(client.getRates(Currency.getBase())).thenReturn(container); 43 | 44 | Map result = ratesService.getCurrentRates(); 45 | verify(client, times(1)).getRates(Currency.getBase()); 46 | 47 | assertEquals(container.getRates().get(Currency.EUR.name()), result.get(Currency.EUR)); 48 | assertEquals(container.getRates().get(Currency.RUB.name()), result.get(Currency.RUB)); 49 | assertEquals(BigDecimal.ONE, result.get(Currency.USD)); 50 | } 51 | 52 | @Test 53 | public void shouldNotRequestRatesWhenTodaysContainerAlreadyExists() { 54 | 55 | ExchangeRatesContainer container = new ExchangeRatesContainer(); 56 | container.setRates(ImmutableMap.of( 57 | Currency.EUR.name(), new BigDecimal("0.8"), 58 | Currency.RUB.name(), new BigDecimal("80") 59 | )); 60 | 61 | when(client.getRates(Currency.getBase())).thenReturn(container); 62 | 63 | // initialize container 64 | ratesService.getCurrentRates(); 65 | 66 | // use existing container 67 | ratesService.getCurrentRates(); 68 | 69 | verify(client, times(1)).getRates(Currency.getBase()); 70 | } 71 | 72 | @Test 73 | public void shouldConvertCurrency() { 74 | 75 | ExchangeRatesContainer container = new ExchangeRatesContainer(); 76 | container.setRates(ImmutableMap.of( 77 | Currency.EUR.name(), new BigDecimal("0.8"), 78 | Currency.RUB.name(), new BigDecimal("80") 79 | )); 80 | 81 | when(client.getRates(Currency.getBase())).thenReturn(container); 82 | 83 | final BigDecimal amount = new BigDecimal(100); 84 | final BigDecimal expectedConvertionResult = new BigDecimal("1.25"); 85 | 86 | BigDecimal result = ratesService.convert(Currency.RUB, Currency.USD, amount); 87 | 88 | assertTrue(expectedConvertionResult.compareTo(result) == 0); 89 | } 90 | 91 | @Test(expected = IllegalArgumentException.class) 92 | public void shouldFailToConvertWhenAmountIsNull() { 93 | ratesService.convert(Currency.EUR, Currency.RUB, null); 94 | } 95 | } -------------------------------------------------------------------------------- /statistics-service/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | hystrix: 2 | command: 3 | default: 4 | execution: 5 | isolation: 6 | thread: 7 | timeoutInMilliseconds: 20000 8 | 9 | spring: 10 | data: 11 | mongodb: 12 | database: piggymetrics 13 | port: 0 14 | 15 | rates: 16 | url: https://api.exchangeratesapi.io -------------------------------------------------------------------------------- /statistics-service/src/test/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | eureka: 2 | client: 3 | enabled: false -------------------------------------------------------------------------------- /turbine-stream-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8-jre 2 | MAINTAINER Chi Dov 3 | 4 | ADD ./target/turbine-stream-service.jar /app/ 5 | CMD ["java", "-Xmx200m", "-jar", "/app/turbine-stream-service.jar"] 6 | 7 | EXPOSE 8989 -------------------------------------------------------------------------------- /turbine-stream-service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | turbine-stream-service 7 | 0.0.1-SNAPSHOT 8 | jar 9 | 10 | turbine-stream-service 11 | Turbine Stream Service 12 | 13 | 14 | com.piggymetrics 15 | piggymetrics 16 | 1.0-SNAPSHOT 17 | 18 | 19 | 20 | 21 | org.springframework.cloud 22 | spring-cloud-starter-config 23 | 24 | 25 | org.springframework.cloud 26 | spring-cloud-starter-netflix-eureka-client 27 | 28 | 29 | org.springframework.cloud 30 | spring-cloud-starter-netflix-turbine-stream 31 | 32 | 33 | org.springframework.cloud 34 | spring-cloud-starter-stream-rabbit 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-starter-test 40 | test 41 | 42 | 43 | 44 | 45 | 46 | 47 | org.springframework.boot 48 | spring-boot-maven-plugin 49 | 50 | ${project.name} 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /turbine-stream-service/src/main/java/com/piggymetrics/turbine/TurbineStreamServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.turbine; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient; 6 | import org.springframework.cloud.netflix.turbine.stream.EnableTurbineStream; 7 | 8 | @SpringBootApplication 9 | @EnableTurbineStream 10 | @EnableDiscoveryClient 11 | public class TurbineStreamServiceApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(TurbineStreamServiceApplication.class, args); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /turbine-stream-service/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: turbine-stream-service 4 | cloud: 5 | config: 6 | uri: http://config:8888 7 | fail-fast: true 8 | password: ${CONFIG_SERVICE_PASSWORD} 9 | username: user -------------------------------------------------------------------------------- /turbine-stream-service/src/test/java/com/piggymetrics/turbine/TurbineStreamServiceApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.piggymetrics.turbine; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class TurbineStreamServiceApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /turbine-stream-service/src/test/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | eureka: 2 | client: 3 | enabled: false --------------------------------------------------------------------------------