├── .github └── workflows │ └── build-master.yml ├── .gitignore ├── LICENCE ├── README.md ├── account-service ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── galleog │ │ │ └── piggymetrics │ │ │ └── account │ │ │ ├── AccountApplication.java │ │ │ ├── config │ │ │ ├── GrpcConfig.java │ │ │ ├── JooqConfig.java │ │ │ └── ReactiveKafkaConfig.java │ │ │ ├── domain │ │ │ ├── Account.java │ │ │ ├── Item.java │ │ │ ├── ItemType.java │ │ │ ├── Saving.java │ │ │ └── TimePeriod.java │ │ │ ├── event │ │ │ └── UserRegisteredEventConsumer.java │ │ │ ├── repository │ │ │ ├── AccountRepository.java │ │ │ └── jooq │ │ │ │ └── JooqAccountRepository.java │ │ │ └── service │ │ │ └── AccountService.java │ ├── proto │ │ ├── AccountService.proto │ │ └── UserRegisteredEvent.proto │ └── resources │ │ ├── application.yml │ │ ├── bootstrap.yml │ │ └── db │ │ └── changelog │ │ └── db.changelog-master.yaml │ └── test │ ├── java │ └── com │ │ └── github │ │ └── galleog │ │ └── piggymetrics │ │ └── account │ │ ├── event │ │ └── UserRegisteredEventConsumerIntegrationTest.java │ │ ├── repository │ │ └── jooq │ │ │ └── JooqAccountRepositoryIntegrationTest.java │ │ └── service │ │ ├── AccountServiceIntegrationTest.java │ │ └── AccountServiceTest.java │ └── resources │ ├── application-test.yml │ ├── bootstrap-test.yml │ └── logback-test.xml ├── api-gateway ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── galleog │ │ │ └── piggymetrics │ │ │ └── apigateway │ │ │ ├── ApiGatewayApplication.java │ │ │ ├── config │ │ │ ├── GrpcConfig.java │ │ │ ├── RouterConfig.java │ │ │ └── SecurityConfig.java │ │ │ ├── handler │ │ │ ├── AccountHandler.java │ │ │ ├── ErrorAttributes.java │ │ │ ├── HandlerUtils.java │ │ │ ├── NotificationHandler.java │ │ │ ├── RestExceptionHandler.java │ │ │ └── StatisticsHandler.java │ │ │ └── model │ │ │ ├── account │ │ │ ├── Account.java │ │ │ ├── Item.java │ │ │ └── Saving.java │ │ │ ├── notification │ │ │ ├── Frequency.java │ │ │ ├── NotificationSettings.java │ │ │ ├── NotificationType.java │ │ │ └── Recipient.java │ │ │ └── statistics │ │ │ ├── DataPoint.java │ │ │ ├── ItemMetric.java │ │ │ └── StatisticalMetric.java │ ├── proto │ │ ├── AccountService.proto │ │ ├── RecipientService.proto │ │ └── StatisticsService.proto │ └── resources │ │ ├── application.yml │ │ └── bootstrap.yml │ └── test │ ├── java │ └── com │ │ └── github │ │ └── galleog │ │ └── piggymetrics │ │ └── apigateway │ │ ├── config │ │ └── GrpcTestConfig.java │ │ └── handler │ │ ├── AccountRequestRouterTest.java │ │ ├── BaseRouterTest.java │ │ ├── NotificationRequestRouterTest.java │ │ ├── RestExceptionHandlerTest.java │ │ └── StatisticsRequestRouterTest.java │ └── resources │ ├── bootstrap-test.yml │ └── logback-test.xml ├── build.gradle ├── charts ├── account-service │ ├── .helmignore │ ├── Chart.yaml │ ├── files │ │ └── application.yml │ ├── templates │ │ ├── _helpers.tpl │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── role.yml │ │ ├── rolebinding.yaml │ │ ├── service.yaml │ │ └── serviceaccount.yaml │ └── values.yaml ├── api-gateway │ ├── .helmignore │ ├── Chart.yaml │ ├── files │ │ └── application.yml │ ├── templates │ │ ├── _helpers.tpl │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── role.yml │ │ ├── rolebinding.yaml │ │ ├── service.yaml │ │ └── serviceaccount.yaml │ └── values.yaml ├── global-values.yaml ├── notification-service │ ├── .helmignore │ ├── Chart.yaml │ ├── files │ │ └── application.yml │ ├── templates │ │ ├── _helpers.tpl │ │ ├── configmap.yaml │ │ ├── deployment.yaml │ │ ├── role.yml │ │ ├── rolebinding.yaml │ │ ├── service.yaml │ │ └── serviceaccount.yaml │ └── values.yaml ├── pgm-dependencies │ ├── .helmignore │ ├── Chart.yaml │ ├── files │ │ ├── keycloak-db.sh │ │ ├── pgm-db.sh │ │ └── pgm-realm.json │ ├── templates │ │ ├── _helpers.tpl │ │ ├── kafka-configmap.yaml │ │ ├── keycloak-admin-creds.yaml │ │ ├── keycloak-configmap.yaml │ │ ├── postgres-configmap.yaml │ │ ├── postgres-init-configmap.yaml │ │ ├── postgres-secret.yaml │ │ └── realm-secret.yaml │ └── values.yaml ├── pgm-frontend │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ ├── deployment.yaml │ │ └── service.yaml │ └── values.yaml ├── piggymetrics │ ├── .helmignore │ ├── Chart.yaml │ ├── files │ │ └── istio-vsvc.yaml │ ├── templates │ │ ├── _helpers.tpl │ │ ├── configmap.yaml │ │ ├── hook-role.yaml │ │ ├── hooks.yaml │ │ └── istio-gateway.yaml │ └── values.yaml └── statistics-service │ ├── .helmignore │ ├── Chart.yaml │ ├── files │ └── application.yml │ ├── templates │ ├── _helpers.tpl │ ├── configmap.yaml │ ├── deployment.yaml │ ├── role.yml │ ├── rolebinding.yaml │ ├── service.yaml │ └── serviceaccount.yaml │ └── values.yaml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── grpc-common ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── galleog │ │ │ ├── grpc │ │ │ └── interceptor │ │ │ │ ├── LogClientInterceptor.java │ │ │ │ └── LogServerInterceptor.java │ │ │ └── protobuf │ │ │ └── java │ │ │ └── type │ │ │ └── converter │ │ │ └── Converters.java │ └── proto │ │ └── protobuf │ │ └── java │ │ └── type │ │ ├── BigDecimal.proto │ │ ├── BigInteger.proto │ │ └── Money.proto │ └── test │ ├── java │ └── com │ │ └── github │ │ └── galleog │ │ └── protobuf │ │ └── java │ │ └── type │ │ └── converter │ │ └── ConvertersTest.java │ └── resources │ └── logback-test.xml ├── keycloak-provider ├── Dockerfile ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── galleog │ │ │ └── piggymetrics │ │ │ └── keycloak │ │ │ └── provider │ │ │ ├── PiggymetricsEventListenerProvider.java │ │ │ ├── PiggymetricsEventListenerProviderFactory.java │ │ │ └── PropertiesReader.java │ ├── proto │ │ └── UserRegisteredEvent.proto │ └── resources │ │ ├── META-INF │ │ └── services │ │ │ └── org.keycloak.events.EventListenerProviderFactory │ │ └── piggymetrics-kafka-producer.properties │ └── test │ ├── java │ └── com │ │ └── github │ │ └── galleog │ │ └── piggymetrics │ │ └── keycloak │ │ └── provider │ │ ├── PiggymetricsEventListenerProviderTest.java │ │ └── PropertiesReaderTest.java │ └── resources │ └── test.properties ├── liquibase-tc ├── build.gradle └── src │ └── main │ ├── java │ └── com │ │ └── github │ │ └── galleog │ │ └── liquibase │ │ └── tc │ │ └── LiquibaseUpdater.java │ └── resources │ └── logback.xml ├── lombok.config ├── notification-service ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── galleog │ │ │ └── piggymetrics │ │ │ └── notification │ │ │ ├── NotificationServiceApplication.java │ │ │ ├── config │ │ │ ├── GrpcConfig.java │ │ │ ├── JooqConfig.java │ │ │ ├── ReactiveKafkaConfig.java │ │ │ └── ScheduledLockConfig.java │ │ │ ├── domain │ │ │ ├── Frequency.java │ │ │ ├── NotificationSettings.java │ │ │ ├── NotificationType.java │ │ │ └── Recipient.java │ │ │ ├── event │ │ │ └── UserRegisteredEventConsumer.java │ │ │ ├── repository │ │ │ ├── RecipientRepository.java │ │ │ └── jooq │ │ │ │ └── JooqRecipientRepository.java │ │ │ └── service │ │ │ ├── EmailService.java │ │ │ ├── NotificationService.java │ │ │ └── RecipientService.java │ ├── proto │ │ ├── AccountService.proto │ │ ├── RecipientService.proto │ │ └── UserRegisteredEvent.proto │ └── resources │ │ ├── application.yml │ │ ├── bootstrap.yml │ │ └── db │ │ └── changelog │ │ └── db.changelog-master.yaml │ └── test │ ├── java │ └── com │ │ └── github │ │ └── galleog │ │ └── piggymetrics │ │ └── notification │ │ ├── domain │ │ └── RecipientTest.java │ │ ├── event │ │ └── UserRegisteredEventConsumerIntegrationTest.java │ │ ├── repository │ │ └── jooq │ │ │ └── JooqRecipientRepositoryIntegrationTest.java │ │ └── service │ │ ├── EmailServiceTest.java │ │ ├── NotificationServiceTest.java │ │ └── RecipientServiceTest.java │ └── resources │ ├── application-test.yml │ ├── bootstrap-test.yml │ ├── logback-test.xml │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── pgm-autoconfigure ├── build.gradle └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── github │ │ │ └── galleog │ │ │ └── piggymetrics │ │ │ └── autoconfigure │ │ │ ├── jooq │ │ │ ├── JooqProperties.java │ │ │ ├── R2dbcJooqAutoConfiguration.java │ │ │ └── TransactionAwareJooqWrapper.java │ │ │ └── kafka │ │ │ ├── ReactiveKafkaAutoConfiguration.java │ │ │ ├── ReactiveKafkaReceiverHelper.java │ │ │ ├── ReceiverOptionsCustomizer.java │ │ │ └── SenderOptionsCustomizer.java │ └── resources │ │ └── META-INF │ │ └── spring.factories │ └── test │ ├── java │ └── com │ │ └── github │ │ └── galleog │ │ └── piggymetrics │ │ └── autoconfigure │ │ ├── jooq │ │ ├── R2dbcJooqAutoConfigurationTest.java │ │ └── TransactionAwareJooqWrapperIntegrationTest.java │ │ └── kafka │ │ ├── ReactiveKafkaAutoConfigurationTest.java │ │ └── ReactiveKafkaReceiverHelperIntegrationTest.java │ └── resources │ ├── application.yml │ ├── logback-test.xml │ └── schema.sql ├── pgm-core ├── build.gradle └── src │ ├── main │ └── java │ │ └── com │ │ └── github │ │ └── galleog │ │ └── piggymetrics │ │ └── core │ │ └── enums │ │ ├── Enum.java │ │ └── json │ │ └── EnumDeserializer.java │ └── test │ ├── java │ └── com │ │ └── github │ │ └── galleog │ │ └── piggymetrics │ │ └── core │ │ └── enums │ │ ├── ColorEnum.java │ │ ├── DuplicatedKeyEnum.java │ │ ├── EnumTest.java │ │ ├── ExtendedGreekLetterEnum.java │ │ ├── GreekLetterEnum.java │ │ ├── InvalidClassEnum.java │ │ ├── NullClassEnum.java │ │ ├── OperationEnum.java │ │ └── json │ │ ├── EnumDeserializerTest.java │ │ ├── EnumSerializerTest.java │ │ ├── IntegerEnum.java │ │ ├── OperationEnum.java │ │ └── TestBean.java │ └── resources │ └── logback-test.xml ├── pgm-frontend ├── Dockerfile ├── nginx.conf └── 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 │ └── keycloak.json ├── settings.gradle ├── skaffold.yaml └── statistics-service ├── build.gradle └── src ├── main ├── java │ └── com │ │ └── github │ │ └── galleog │ │ └── piggymetrics │ │ └── statistics │ │ ├── StatisticsApplication.java │ │ ├── config │ │ ├── GrpcConfig.java │ │ ├── JooqConfig.java │ │ └── ReactiveKafkaConfig.java │ │ ├── domain │ │ ├── DataPoint.java │ │ ├── ItemMetric.java │ │ ├── ItemType.java │ │ ├── StatisticalMetric.java │ │ └── TimePeriod.java │ │ ├── event │ │ └── AccountUpdatedEventConsumer.java │ │ ├── repository │ │ ├── DataPointRepository.java │ │ └── jooq │ │ │ └── JooqDataPointRepository.java │ │ └── service │ │ ├── MonetaryConversionService.java │ │ └── StatisticsService.java ├── proto │ ├── AccountService.proto │ └── StatisticsService.proto └── resources │ ├── application.yml │ ├── bootstrap.yml │ └── db │ └── changelog │ └── db.changelog-master.yaml └── test ├── java └── com │ └── github │ └── galleog │ └── piggymetrics │ └── statistics │ ├── event │ └── AccountUpdatedEventConsumerIntegrationTest.java │ ├── repository │ └── jooq │ │ └── JooqDataPointRepositoryIntegrationTest.java │ └── service │ └── StatisticsServiceTest.java └── resources ├── application-test.yml ├── bootstrap-test.yml └── logback-test.xml /.gitignore: -------------------------------------------------------------------------------- 1 | # Intellij 2 | .idea/ 3 | *.iml 4 | out/ 5 | 6 | # Mac 7 | .DS_Store 8 | 9 | # Gradle 10 | .gradle/ 11 | build/ 12 | 13 | # Java 14 | .resourceCache/ 15 | 16 | # Helm 17 | Chart.lock 18 | charts/**/charts/ 19 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Oleg Galkin 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/src/main/java/com/github/galleog/piggymetrics/account/AccountApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.account; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 6 | import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; 7 | import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; 8 | import org.springframework.transaction.annotation.EnableTransactionManagement; 9 | 10 | /** 11 | * Main Spring Boot application class. 12 | */ 13 | @EnableTransactionManagement 14 | @SpringBootApplication(exclude = { 15 | KafkaAutoConfiguration.class, 16 | DataSourceAutoConfiguration.class, 17 | DataSourceTransactionManagerAutoConfiguration.class 18 | }) 19 | public class AccountApplication { 20 | public static void main(String[] args) { 21 | SpringApplication.run(AccountApplication.class, args); 22 | } 23 | } -------------------------------------------------------------------------------- /account-service/src/main/java/com/github/galleog/piggymetrics/account/config/GrpcConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.account.config; 2 | 3 | import com.github.galleog.grpc.interceptor.LogServerInterceptor; 4 | import io.grpc.ServerInterceptor; 5 | import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | 9 | /** 10 | * Configuration for gRPC. 11 | */ 12 | @Profile("!test") 13 | @Configuration(proxyBeanMethods = false) 14 | public class GrpcConfig { 15 | @GrpcGlobalServerInterceptor 16 | ServerInterceptor logServerInterceptor() { 17 | return new LogServerInterceptor(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/github/galleog/piggymetrics/account/config/JooqConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.account.config; 2 | 3 | import static com.github.galleog.piggymetrics.account.domain.Public.PUBLIC; 4 | 5 | import com.github.galleog.piggymetrics.autoconfigure.jooq.JooqProperties; 6 | import org.jooq.conf.MappedSchema; 7 | import org.jooq.conf.RenderMapping; 8 | import org.jooq.conf.Settings; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Profile; 12 | 13 | /** 14 | * Configures database schema for jOOQ. 15 | */ 16 | @Profile("!test") 17 | @Configuration(proxyBeanMethods = false) 18 | public class JooqConfig { 19 | @Bean 20 | Settings settings(JooqProperties properties) { 21 | return new Settings() 22 | .withRenderMapping( 23 | new RenderMapping() 24 | .withSchemata( 25 | new MappedSchema() 26 | .withInput(PUBLIC.getName()) 27 | .withOutput(properties.getSchema()) 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/github/galleog/piggymetrics/account/config/ReactiveKafkaConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.account.config; 2 | 3 | import com.github.daniel.shuy.kafka.protobuf.serde.KafkaProtobufDeserializer; 4 | import com.github.galleog.piggymetrics.account.event.UserRegisteredEventConsumer; 5 | import com.github.galleog.piggymetrics.auth.grpc.UserRegisteredEventProto.UserRegisteredEvent; 6 | import com.github.galleog.piggymetrics.autoconfigure.kafka.ReactiveKafkaReceiverHelper; 7 | import com.github.galleog.piggymetrics.autoconfigure.kafka.ReceiverOptionsCustomizer; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.kafka.core.reactive.ReactiveKafkaConsumerTemplate; 11 | 12 | /** 13 | * Configuration for reactive Kafka. 14 | */ 15 | @Configuration(proxyBeanMethods = false) 16 | public class ReactiveKafkaConfig { 17 | @Bean 18 | ReceiverOptionsCustomizer receiverOptionsCustomizer() { 19 | return options -> options.withValueDeserializer(new KafkaProtobufDeserializer<>(UserRegisteredEvent.parser())); 20 | } 21 | 22 | @Bean 23 | ReactiveKafkaReceiverHelper receiverHelper( 24 | ReactiveKafkaConsumerTemplate consumerTemplate, 25 | UserRegisteredEventConsumer consumer 26 | ) { 27 | return new ReactiveKafkaReceiverHelper<>(consumerTemplate, consumer); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/github/galleog/piggymetrics/account/domain/Account.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.account.domain; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.Singular; 7 | import org.apache.commons.lang3.Validate; 8 | import org.apache.commons.lang3.builder.ToStringBuilder; 9 | import org.springframework.lang.NonNull; 10 | import org.springframework.lang.Nullable; 11 | 12 | import java.time.LocalDateTime; 13 | import java.util.Collection; 14 | import java.util.List; 15 | 16 | /** 17 | * Entity for accounts. 18 | */ 19 | @Getter 20 | public class Account { 21 | /** 22 | * Name of the user the account belongs to. 23 | */ 24 | private String name; 25 | /** 26 | * Account incomes and expenses. 27 | */ 28 | private List items; 29 | /** 30 | * Account savings. 31 | */ 32 | private Saving saving; 33 | /** 34 | * Date when the account was last changed. 35 | */ 36 | private LocalDateTime updateTime; 37 | /** 38 | * Additional note. 39 | */ 40 | private String note; 41 | 42 | @Builder 43 | @SuppressWarnings("unused") 44 | private Account(@NonNull String name, @NonNull @Singular Collection items, 45 | @NonNull Saving saving, @Nullable String note, @Nullable LocalDateTime updateTime) { 46 | setName(name); 47 | setItems(items); 48 | setSaving(saving); 49 | setNote(note); 50 | setUpdateTime(updateTime); 51 | } 52 | 53 | private void setName(String name) { 54 | Validate.notBlank(name); 55 | this.name = name; 56 | } 57 | 58 | private void setItems(Collection items) { 59 | Validate.noNullElements(items); 60 | this.items = ImmutableList.copyOf(items); 61 | } 62 | 63 | private void setSaving(Saving saving) { 64 | Validate.notNull(saving); 65 | this.saving = saving; 66 | } 67 | 68 | private void setNote(String note) { 69 | Validate.isTrue(note == null || note.length() <= 20); 70 | this.note = note; 71 | } 72 | 73 | private void setUpdateTime(LocalDateTime updateTime) { 74 | this.updateTime = updateTime; 75 | } 76 | 77 | @Override 78 | public String toString() { 79 | return new ToStringBuilder(this) 80 | .append(getName()) 81 | .build(); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/github/galleog/piggymetrics/account/domain/Item.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.account.domain; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import org.apache.commons.lang3.Validate; 6 | import org.apache.commons.lang3.builder.ToStringBuilder; 7 | import org.javamoney.moneta.Money; 8 | import org.springframework.lang.NonNull; 9 | import org.springframework.lang.Nullable; 10 | 11 | /** 12 | * Entity for an income or expense item. 13 | */ 14 | @Getter 15 | public class Item { 16 | /** 17 | * Identifier of the item. 18 | */ 19 | private Long id; 20 | /** 21 | * Item type. 22 | */ 23 | private ItemType type; 24 | /** 25 | * Item title. 26 | */ 27 | private String title; 28 | /** 29 | * Item monetary amount. 30 | */ 31 | private Money moneyAmount; 32 | /** 33 | * Item period. 34 | */ 35 | private TimePeriod period; 36 | /** 37 | * Item icon. 38 | */ 39 | private String icon; 40 | 41 | @Builder 42 | private Item(@Nullable Long id, @NonNull ItemType type, @NonNull String title, @NonNull Money moneyAmount, 43 | @NonNull TimePeriod period, @NonNull String icon) { 44 | setId(id); 45 | setType(type); 46 | setTitle(title); 47 | setMoneyAmount(moneyAmount); 48 | setPeriod(period); 49 | setIcon(icon); 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return new ToStringBuilder(this) 55 | .append("id", getId()) 56 | .append("type", getType()) 57 | .append("title", getTitle()) 58 | .build(); 59 | } 60 | 61 | private void setId(Long id) { 62 | this.id = id; 63 | } 64 | 65 | private void setType(ItemType type) { 66 | Validate.notNull(type); 67 | this.type = type; 68 | } 69 | 70 | private void setTitle(String title) { 71 | Validate.notBlank(title); 72 | Validate.isTrue(title.length() <= 20); 73 | this.title = title; 74 | } 75 | 76 | private void setMoneyAmount(Money moneyAmount) { 77 | Validate.notNull(moneyAmount); 78 | Validate.isTrue(moneyAmount.isPositive()); 79 | this.moneyAmount = moneyAmount; 80 | } 81 | 82 | private void setPeriod(TimePeriod period) { 83 | Validate.notNull(period); 84 | this.period = period; 85 | } 86 | 87 | private void setIcon(String icon) { 88 | Validate.notBlank(icon); 89 | this.icon = icon; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/github/galleog/piggymetrics/account/domain/ItemType.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.account.domain; 2 | 3 | /** 4 | * Type of item. 5 | */ 6 | public enum ItemType { 7 | INCOME, 8 | EXPENSE 9 | } 10 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/github/galleog/piggymetrics/account/domain/Saving.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.account.domain; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import org.apache.commons.lang3.Validate; 6 | import org.javamoney.moneta.Money; 7 | import org.springframework.lang.NonNull; 8 | 9 | import java.math.BigDecimal; 10 | 11 | /** 12 | * Entity for savings. 13 | */ 14 | public class Saving { 15 | /** 16 | * Saving monetary amount. 17 | */ 18 | @Getter 19 | private Money moneyAmount; 20 | /** 21 | * Saving interest. 22 | */ 23 | @Getter 24 | private BigDecimal interest; 25 | /** 26 | * Indicates if the saving is a deposit. 27 | */ 28 | @Getter 29 | private boolean deposit; 30 | /** 31 | * Indicates if the saving has capitalization. 32 | */ 33 | @Getter 34 | private boolean capitalization; 35 | 36 | @Builder 37 | @SuppressWarnings("unused") 38 | private Saving(@NonNull Money moneyAmount, @NonNull BigDecimal interest, 39 | boolean deposit, boolean capitalization) { 40 | setMoneyAmount(moneyAmount); 41 | setInterest(interest); 42 | setDeposit(deposit); 43 | setCapitalization(capitalization); 44 | } 45 | 46 | private void setMoneyAmount(Money moneyAmount) { 47 | Validate.notNull(moneyAmount); 48 | Validate.isTrue(moneyAmount.isPositiveOrZero()); 49 | this.moneyAmount = moneyAmount; 50 | } 51 | 52 | private void setInterest(BigDecimal interest) { 53 | Validate.notNull(interest); 54 | Validate.isTrue(interest.signum() >= 0); 55 | this.interest = interest; 56 | } 57 | 58 | private void setDeposit(boolean deposit) { 59 | this.deposit = deposit; 60 | } 61 | 62 | private void setCapitalization(boolean capitalization) { 63 | this.capitalization = capitalization; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/github/galleog/piggymetrics/account/domain/TimePeriod.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.account.domain; 2 | 3 | /** 4 | * Time period values. 5 | */ 6 | @SuppressWarnings("unused") 7 | public enum TimePeriod { 8 | YEAR, QUARTER, MONTH, DAY, HOUR 9 | } 10 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/github/galleog/piggymetrics/account/event/UserRegisteredEventConsumer.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.account.event; 2 | 3 | import com.github.galleog.piggymetrics.account.domain.Account; 4 | import com.github.galleog.piggymetrics.account.domain.Saving; 5 | import com.github.galleog.piggymetrics.account.repository.AccountRepository; 6 | import com.github.galleog.piggymetrics.auth.grpc.UserRegisteredEventProto.UserRegisteredEvent; 7 | import com.google.common.annotations.VisibleForTesting; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.apache.kafka.clients.consumer.ConsumerRecord; 11 | import org.javamoney.moneta.Money; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.transaction.reactive.TransactionalOperator; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | import javax.money.CurrencyUnit; 18 | import javax.money.Monetary; 19 | import java.math.BigDecimal; 20 | import java.util.function.Function; 21 | 22 | /** 23 | * Consumer of events on new user registrations. 24 | */ 25 | @Slf4j 26 | @Component 27 | @RequiredArgsConstructor 28 | public class UserRegisteredEventConsumer implements Function>, Mono> { 29 | @VisibleForTesting 30 | public static final CurrencyUnit BASE_CURRENCY = Monetary.getCurrency("USD"); 31 | 32 | private final AccountRepository accountRepository; 33 | private final TransactionalOperator operator; 34 | 35 | @Override 36 | public Mono apply(Flux> records) { 37 | return records.map(record -> record.value().getUserName()) 38 | .doOnNext(name -> logger.info("UserRegisteredEvent for user '{}' received", name)) 39 | .flatMap(this::doCreateAccount) 40 | .then(); 41 | } 42 | 43 | private Mono doCreateAccount(String name) { 44 | return accountRepository.getByName(name) 45 | .doOnNext(account -> logger.warn("Account for user '{}' already exists", name)) 46 | .hasElement() 47 | .filter(b -> !b) 48 | .map(b -> newAccount(name)) 49 | .flatMap(accountRepository::save) 50 | .doOnNext(account -> logger.info("Account for user '{}' created", account.getName())) 51 | .as(operator::transactional); 52 | } 53 | 54 | private Account newAccount(String name) { 55 | var saving = Saving.builder() 56 | .moneyAmount(Money.of(BigDecimal.ZERO, BASE_CURRENCY)) 57 | .interest(BigDecimal.ZERO) 58 | .deposit(false) 59 | .capitalization(false) 60 | .build(); 61 | return Account.builder() 62 | .name(name) 63 | .saving(saving) 64 | .build(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /account-service/src/main/java/com/github/galleog/piggymetrics/account/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.account.repository; 2 | 3 | import com.github.galleog.piggymetrics.account.domain.Account; 4 | import org.springframework.lang.NonNull; 5 | import reactor.core.publisher.Mono; 6 | 7 | /** 8 | * Repository for {@link Account}. 9 | */ 10 | public interface AccountRepository { 11 | /** 12 | * Gets an account by its name. 13 | * 14 | * @param name the account name 15 | * @return the account with the specified name 16 | */ 17 | Mono getByName(@NonNull String name); 18 | 19 | /** 20 | * Saves an account. 21 | * 22 | * @param account the account to save 23 | * @return the saved account 24 | */ 25 | Mono save(@NonNull Account account); 26 | 27 | /** 28 | * Updates an account. 29 | * 30 | * @param account the account to update 31 | * @return the updated account 32 | */ 33 | Mono update(@NonNull Account account); 34 | } 35 | -------------------------------------------------------------------------------- /account-service/src/main/proto/UserRegisteredEvent.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package piggymetrics.auth; 4 | 5 | option java_package = "com.github.galleog.piggymetrics.auth.grpc"; 6 | option java_outer_classname = "UserRegisteredEventProto"; 7 | 8 | // Event sent when a user is registered. 9 | message UserRegisteredEvent { 10 | // Required. Identifier of the registered user. 11 | string user_id = 1; 12 | // Required. Name of the registered user. 13 | string user_name = 2; 14 | // Required. User email. 15 | string email = 3; 16 | } -------------------------------------------------------------------------------- /account-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | r2dbc: 3 | url: r2dbc:pool:postgresql://${DATABASE_HOST:localhost}:${DATABASE_PORT:5432}/${DATABASE_NAME:piggymetrics} 4 | username: ${DATABASE_USER:postgres} 5 | password: ${DATABASE_PASSWORD:secret} 6 | 7 | jmx: 8 | enabled: false 9 | 10 | sql: 11 | init: 12 | mode: never 13 | 14 | jooq: 15 | schema: ${DATABASE_SCHEMA:account_service} 16 | sql-dialect: postgres 17 | 18 | liquibase: 19 | default-schema: ${DATABASE_SCHEMA:account_service} 20 | driver-class-name: org.postgresql.Driver 21 | url: jdbc:postgresql://${DATABASE_HOST:localhost}:${DATABASE_PORT:5432}/${DATABASE_NAME:piggymetrics} 22 | user: ${DATABASE_USER:postgres} 23 | password: ${DATABASE_PASSWORD:secret} 24 | 25 | kafka: 26 | bootstrap-servers: ${KAFKA_BROKERS:localhost:9092} 27 | producer: 28 | acks: -1 29 | retries: 5 30 | value-serializer: com.github.daniel.shuy.kafka.protobuf.serde.KafkaProtobufSerializer 31 | properties: 32 | "enable.idempotence": true 33 | topic: ${ACCOUNT_EVENTS_TOPIC:account-events} 34 | consumer: 35 | subscribeTopics: ${USER_EVENTS_TOPIC:user-events} 36 | group-id: account-service 37 | 38 | grpc: 39 | server: 40 | port: 9090 -------------------------------------------------------------------------------- /account-service/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: account-service 4 | -------------------------------------------------------------------------------- /account-service/src/test/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | main: 3 | banner-mode: off 4 | 5 | datasource: 6 | type: org.postgresql.ds.PGSimpleDataSource 7 | 8 | liquibase: 9 | default-schema: 10 | 11 | grpc: 12 | server: 13 | port: -1 14 | in-process-name: account-service 15 | 16 | client: 17 | account-service: 18 | address: "in-process:account-service" -------------------------------------------------------------------------------- /account-service/src/test/resources/bootstrap-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | cloud: 3 | kubernetes: 4 | enabled: false -------------------------------------------------------------------------------- /account-service/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /api-gateway/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'org.springframework.boot' 2 | apply plugin: 'com.google.protobuf' 3 | apply plugin: 'com.google.cloud.tools.jib' 4 | 5 | description = 'api-gateway' 6 | 7 | dependencies { 8 | implementation( 9 | project(':pgm-core'), 10 | project(':grpc-common'), 11 | 'org.springframework.boot:spring-boot-starter-webflux', 12 | 'org.springframework.boot:spring-boot-starter-security', 13 | 'org.springframework.boot:spring-boot-starter-actuator', 14 | 'org.springframework.boot:spring-boot-autoconfigure', 15 | 'org.springframework.cloud:spring-cloud-starter-kubernetes-fabric8-config', 16 | 'org.springframework.security:spring-security-oauth2-resource-server', 17 | 'org.springframework.security:spring-security-oauth2-jose', 18 | 'io.projectreactor:reactor-core', 19 | 'io.grpc:grpc-protobuf', 20 | 'io.grpc:grpc-stub', 21 | 'com.salesforce.servicelibs:reactor-grpc-stub', 22 | 'net.devh:grpc-client-spring-boot-starter', 23 | 'org.apache.commons:commons-lang3', 24 | 'org.javamoney:moneta', 25 | 'org.zalando:jackson-datatype-money', 26 | 'com.google.guava:guava' 27 | ) 28 | 29 | runtimeOnly( 30 | 'org.slf4j:jul-to-slf4j' 31 | ) 32 | 33 | testImplementation( 34 | 'org.springframework.boot:spring-boot-starter-test', 35 | 'org.springframework.security:spring-security-test', 36 | 'io.projectreactor:reactor-test', 37 | 'com.google.protobuf:protobuf-java-util', 38 | 'com.asarkar.grpc:grpc-test', 39 | 'org.junit.jupiter:junit-jupiter' 40 | ) 41 | } 42 | 43 | protobuf { 44 | protoc { 45 | artifact = "com.google.protobuf:protoc:${ver.protobuf}" 46 | } 47 | plugins { 48 | grpc { 49 | artifact = "io.grpc:protoc-gen-grpc-java:${ver.grpc}" 50 | } 51 | reactor { 52 | artifact = "com.salesforce.servicelibs:reactor-grpc:${ver.reactiveGrpc}" 53 | } 54 | } 55 | generateProtoTasks { 56 | all()*.plugins { 57 | grpc { 58 | option 'enable_deprecated=false' 59 | } 60 | reactor {} 61 | } 62 | } 63 | } 64 | 65 | idea { 66 | module { 67 | sourceDirs += file("$buildDir/generated/source/proto/main/java") 68 | sourceDirs += file("$buildDir/generated/source/proto/main/grpc") 69 | sourceDirs += file("$buildDir/generated/source/proto/main/reactor") 70 | generatedSourceDirs += file("$buildDir/generated/source/proto/main/java") 71 | generatedSourceDirs += file("$buildDir/generated/source/proto/main/grpc") 72 | generatedSourceDirs += file("$buildDir/generated/source/proto/main/reactor") 73 | } 74 | } 75 | 76 | jib { 77 | from { 78 | image = 'openjdk:11-jre-slim' 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/ApiGatewayApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; 6 | import org.springframework.context.annotation.Bean; 7 | import org.zalando.jackson.datatype.money.MoneyModule; 8 | 9 | /** 10 | * Main Spring Boot application class. 11 | */ 12 | @SpringBootApplication 13 | public class ApiGatewayApplication { 14 | public static void main(String[] args) { 15 | SpringApplication.run(ApiGatewayApplication.class, args); 16 | } 17 | 18 | @Bean 19 | public Jackson2ObjectMapperBuilderCustomizer objectMapperBuilderCustomizer() { 20 | return builder -> builder.modulesToInstall(new MoneyModule()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/config/GrpcConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.config; 2 | 3 | import com.github.galleog.grpc.interceptor.LogClientInterceptor; 4 | import io.grpc.ClientInterceptor; 5 | import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | 9 | /** 10 | * Configuration for gRPC. 11 | */ 12 | @Profile("!test") 13 | @Configuration(proxyBeanMethods = false) 14 | public class GrpcConfig { 15 | @GrpcGlobalClientInterceptor 16 | public ClientInterceptor logClientInterceptor() { 17 | return new LogClientInterceptor(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/config/RouterConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.config; 2 | 3 | import static org.springframework.web.reactive.function.server.RequestPredicates.contentType; 4 | import static org.springframework.web.reactive.function.server.RouterFunctions.route; 5 | 6 | import com.github.galleog.piggymetrics.apigateway.handler.AccountHandler; 7 | import com.github.galleog.piggymetrics.apigateway.handler.NotificationHandler; 8 | import com.github.galleog.piggymetrics.apigateway.handler.StatisticsHandler; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.web.reactive.function.server.RouterFunction; 13 | import org.springframework.web.reactive.function.server.ServerResponse; 14 | 15 | /** 16 | * Configuration for Web routing. 17 | */ 18 | @Configuration(proxyBeanMethods = false) 19 | public class RouterConfig { 20 | public static final String DEMO_ACCOUNT = "demo"; 21 | 22 | @Bean 23 | public RouterFunction routeAccountRequests(AccountHandler handler) { 24 | return route().path("/accounts", builder -> 25 | builder.GET("/demo", request -> handler.getDemoAccount()) 26 | .GET("/current", handler::getCurrentAccount) 27 | .PUT("/current", contentType(MediaType.APPLICATION_JSON), handler::updateCurrentAccount) 28 | ).build(); 29 | } 30 | 31 | @Bean 32 | public RouterFunction routeNotificationRequests(NotificationHandler handler) { 33 | return route().path("/notifications", builder -> 34 | builder.GET("/current", handler::getCurrentNotificationsSettings) 35 | .PUT("/current", contentType(MediaType.APPLICATION_JSON), handler::updateCurrentNotificationsSettings) 36 | ).build(); 37 | } 38 | 39 | @Bean 40 | public RouterFunction routeDataPointRequests(StatisticsHandler handler) { 41 | return route().path("/statistics", builder -> 42 | builder.GET("/demo", request -> handler.getDemoStatistics()) 43 | .GET("/current", handler::getCurrentAccountStatistics) 44 | 45 | ).build(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; 6 | import org.springframework.security.config.web.server.ServerHttpSecurity; 7 | import org.springframework.security.web.server.SecurityWebFilterChain; 8 | 9 | /** 10 | * Security configuration. 11 | */ 12 | @EnableWebFluxSecurity 13 | @Configuration(proxyBeanMethods = false) 14 | public class SecurityConfig { 15 | @Bean 16 | public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) { 17 | http.authorizeExchange() 18 | .pathMatchers("/accounts/demo", "/statistics/demo", "/actuator/**").permitAll() 19 | .anyExchange().authenticated() 20 | .and() 21 | .oauth2ResourceServer() 22 | .jwt(); 23 | return http.build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/handler/ErrorAttributes.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.handler; 2 | 3 | import io.grpc.Status; 4 | import org.springframework.boot.web.error.ErrorAttributeOptions; 5 | import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; 6 | import org.springframework.core.codec.CodecException; 7 | import org.springframework.http.HttpStatus; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.reactive.function.server.ServerRequest; 10 | import org.springframework.web.server.ResponseStatusException; 11 | 12 | import java.util.Map; 13 | 14 | /** 15 | * {@link DefaultErrorAttributes} that handles gRPC-specific errors. 16 | */ 17 | @Component 18 | public class ErrorAttributes extends DefaultErrorAttributes { 19 | static final String STATUS_KEY = "status"; 20 | private static final String ERROR_KEY = "error"; 21 | 22 | @Override 23 | public Map getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) { 24 | Map result = super.getErrorAttributes(request, options); 25 | if ((int) result.get(STATUS_KEY) == HttpStatus.INTERNAL_SERVER_ERROR.value()) { 26 | HttpStatus status = errorStatus(getError(request)); 27 | result.put(STATUS_KEY, status.value()); 28 | result.put(ERROR_KEY, status.getReasonPhrase()); 29 | } 30 | return result; 31 | } 32 | 33 | private HttpStatus errorStatus(Throwable error) { 34 | if (error instanceof ResponseStatusException) { 35 | return ((ResponseStatusException) error).getStatus(); 36 | } 37 | if (error instanceof CodecException) { 38 | return HttpStatus.BAD_REQUEST; 39 | } 40 | 41 | Status status = Status.fromThrowable(error); 42 | switch (status.getCode()) { 43 | case ALREADY_EXISTS: 44 | case INVALID_ARGUMENT: 45 | return HttpStatus.BAD_REQUEST; 46 | case UNAUTHENTICATED: 47 | return HttpStatus.UNAUTHORIZED; 48 | case PERMISSION_DENIED: 49 | return HttpStatus.FORBIDDEN; 50 | case NOT_FOUND: 51 | return HttpStatus.NOT_FOUND; 52 | default: 53 | return HttpStatus.INTERNAL_SERVER_ERROR; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/handler/HandlerUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.handler; 2 | 3 | import com.google.common.annotations.VisibleForTesting; 4 | import org.springframework.security.core.Authentication; 5 | import org.springframework.security.oauth2.jwt.Jwt; 6 | import org.springframework.web.reactive.function.server.ServerRequest; 7 | import reactor.core.publisher.Mono; 8 | 9 | /** 10 | * Helper for request handlers. 11 | */ 12 | abstract class HandlerUtils { 13 | @VisibleForTesting 14 | static final String USERNAME_CLAIM = "preferred_username"; 15 | 16 | /** 17 | * Gets the name of the current user. 18 | * 19 | * @param request the server request 20 | */ 21 | static Mono getCurrentUser(ServerRequest request) { 22 | return request.principal() 23 | .cast(Authentication.class) 24 | .map(authentication -> (Jwt) authentication.getPrincipal()) 25 | .map(jwt -> jwt.getClaimAsString(USERNAME_CLAIM)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/handler/RestExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.handler; 2 | 3 | import static org.springframework.web.reactive.function.server.RequestPredicates.all; 4 | import static org.springframework.web.reactive.function.server.RouterFunctions.route; 5 | 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.boot.autoconfigure.web.ServerProperties; 8 | import org.springframework.boot.autoconfigure.web.WebProperties; 9 | import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler; 10 | import org.springframework.boot.web.reactive.error.ErrorAttributes; 11 | import org.springframework.context.ApplicationContext; 12 | import org.springframework.core.annotation.Order; 13 | import org.springframework.http.codec.ServerCodecConfigurer; 14 | import org.springframework.http.server.reactive.ServerHttpRequest; 15 | import org.springframework.stereotype.Component; 16 | import org.springframework.web.reactive.function.server.RouterFunction; 17 | import org.springframework.web.reactive.function.server.ServerResponse; 18 | import org.springframework.web.server.ServerWebExchange; 19 | import reactor.core.publisher.Mono; 20 | 21 | /** 22 | * Custom {@link org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler}. 23 | */ 24 | @Slf4j 25 | @Component 26 | @Order(-1) 27 | public class RestExceptionHandler extends DefaultErrorWebExceptionHandler { 28 | public RestExceptionHandler(ErrorAttributes errorAttributes, WebProperties properties, ServerProperties serverProperties, 29 | ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) { 30 | super(errorAttributes, properties.getResources(), serverProperties.getError(), applicationContext); 31 | setMessageReaders(serverCodecConfigurer.getReaders()); 32 | setMessageWriters(serverCodecConfigurer.getWriters()); 33 | } 34 | 35 | @Override 36 | protected RouterFunction getRoutingFunction(ErrorAttributes errorAttributes) { 37 | return route(all(), this::renderErrorResponse); 38 | } 39 | 40 | @Override 41 | public Mono handle(ServerWebExchange exchange, Throwable throwable) { 42 | Mono result = super.handle(exchange, throwable); 43 | logger.warn(formatError(throwable, exchange.getRequest()), throwable); 44 | return result; 45 | } 46 | 47 | private String formatError(Throwable error, ServerHttpRequest request) { 48 | String reason = error.getClass().getSimpleName() + ": " + error.getMessage(); 49 | String path = request.getURI().getRawPath(); 50 | return "Resolved [" + reason + "] for HTTP " + request.getMethod() + " " + path; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/model/account/Account.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.model.account; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; 5 | import com.google.common.collect.ImmutableSet; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.Singular; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.apache.commons.lang3.Validate; 11 | import org.springframework.lang.NonNull; 12 | import org.springframework.lang.Nullable; 13 | 14 | import java.time.LocalDateTime; 15 | import java.util.Set; 16 | 17 | /** 18 | * Model class for accounts. 19 | */ 20 | @Getter 21 | @JsonDeserialize(builder = Account.AccountBuilder.class) 22 | public class Account { 23 | /** 24 | * Account name. 25 | */ 26 | @Nullable 27 | private String name; 28 | /** 29 | * Account incomes and responses. 30 | */ 31 | @NonNull 32 | private Set items; 33 | /** 34 | * Account savings. 35 | */ 36 | @NonNull 37 | private Saving saving; 38 | /** 39 | * Additional note. 40 | */ 41 | @Nullable 42 | private String note; 43 | /** 44 | * Date when the account was last changed. 45 | */ 46 | @Nullable 47 | private LocalDateTime updateTime; 48 | 49 | @Builder 50 | private Account(@Nullable String name, @NonNull @Singular Set items, @NonNull Saving saving, 51 | @Nullable String note, @Nullable LocalDateTime updateTime) { 52 | setName(name); 53 | setItems(items); 54 | setSaving(saving); 55 | setNote(note); 56 | setUpdateTime(updateTime); 57 | } 58 | 59 | private void setName(String name) { 60 | this.name = name; 61 | } 62 | 63 | private void setItems(Set items) { 64 | Validate.noNullElements(items); 65 | this.items = ImmutableSet.copyOf(items); 66 | } 67 | 68 | private void setSaving(Saving saving) { 69 | Validate.notNull(saving); 70 | this.saving = saving; 71 | } 72 | 73 | private void setNote(String note) { 74 | this.note = note; 75 | } 76 | 77 | private void setUpdateTime(LocalDateTime updateTime) { 78 | this.updateTime = updateTime; 79 | } 80 | 81 | @JsonPOJOBuilder(withPrefix = StringUtils.EMPTY) 82 | public static final class AccountBuilder { 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/model/account/Item.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.model.account; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; 5 | import com.github.galleog.piggymetrics.account.grpc.AccountServiceProto.ItemType; 6 | import com.github.galleog.piggymetrics.account.grpc.AccountServiceProto.TimePeriod; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.apache.commons.lang3.Validate; 11 | import org.javamoney.moneta.Money; 12 | import org.springframework.lang.NonNull; 13 | 14 | /** 15 | * Model class for expenses and incomes. 16 | */ 17 | @Getter 18 | @JsonDeserialize(builder = Item.ItemBuilder.class) 19 | public class Item { 20 | /** 21 | * Item type. 22 | */ 23 | @NonNull 24 | private ItemType type; 25 | /** 26 | * Item title. 27 | */ 28 | @NonNull 29 | private String title; 30 | /** 31 | * Item monetary amount. 32 | */ 33 | @NonNull 34 | private Money moneyAmount; 35 | /** 36 | * Item period. 37 | */ 38 | @NonNull 39 | private TimePeriod period; 40 | /** 41 | * Item icon. 42 | */ 43 | @NonNull 44 | private String icon; 45 | 46 | @Builder 47 | private Item(@NonNull ItemType type, @NonNull String title, @NonNull Money moneyAmount, 48 | @NonNull TimePeriod period, @NonNull String icon) { 49 | setType(type); 50 | setTitle(title); 51 | setMoneyAmount(moneyAmount); 52 | setPeriod(period); 53 | setIcon(icon); 54 | } 55 | 56 | private void setType(ItemType type) { 57 | Validate.notNull(type); 58 | this.type = type; 59 | } 60 | 61 | private void setTitle(String title) { 62 | Validate.notBlank(title); 63 | Validate.isTrue(title.length() <= 20); 64 | this.title = title; 65 | } 66 | 67 | private void setMoneyAmount(Money moneyAmount) { 68 | Validate.notNull(moneyAmount); 69 | Validate.isTrue(moneyAmount.isPositive()); 70 | this.moneyAmount = moneyAmount; 71 | } 72 | 73 | private void setPeriod(TimePeriod period) { 74 | Validate.notNull(period); 75 | this.period = period; 76 | } 77 | 78 | private void setIcon(String icon) { 79 | Validate.notBlank(icon); 80 | this.icon = icon; 81 | } 82 | 83 | @JsonPOJOBuilder(withPrefix = StringUtils.EMPTY) 84 | public static final class ItemBuilder { 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/model/account/Saving.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.model.account; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.apache.commons.lang3.Validate; 9 | import org.javamoney.moneta.Money; 10 | import org.springframework.lang.NonNull; 11 | 12 | import java.math.BigDecimal; 13 | 14 | /** 15 | * Model class for savings. 16 | */ 17 | @Getter 18 | @JsonDeserialize(builder = Saving.SavingBuilder.class) 19 | public class Saving { 20 | /** 21 | * Saving monetary amount. 22 | */ 23 | @NonNull 24 | private Money moneyAmount; 25 | /** 26 | * Saving interest. 27 | */ 28 | @NonNull 29 | private BigDecimal interest; 30 | /** 31 | * Indicates if the saving is a deposit. 32 | */ 33 | private boolean deposit; 34 | /** 35 | * Indicates if the saving has capitalization. 36 | */ 37 | private boolean capitalization; 38 | 39 | @Builder 40 | private Saving(@NonNull Money moneyAmount, @NonNull BigDecimal interest, 41 | boolean deposit, boolean capitalization) { 42 | setMoneyAmount(moneyAmount); 43 | setInterest(interest); 44 | setDeposit(deposit); 45 | setCapitalization(capitalization); 46 | } 47 | 48 | private void setMoneyAmount(Money moneyAmount) { 49 | Validate.notNull(moneyAmount); 50 | Validate.isTrue(moneyAmount.isPositiveOrZero()); 51 | this.moneyAmount = moneyAmount; 52 | } 53 | 54 | private void setInterest(BigDecimal interest) { 55 | Validate.notNull(interest); 56 | Validate.isTrue(interest.signum() >= 0); 57 | this.interest = interest; 58 | } 59 | 60 | private void setDeposit(boolean deposit) { 61 | this.deposit = deposit; 62 | } 63 | 64 | private void setCapitalization(boolean capitalization) { 65 | this.capitalization = capitalization; 66 | } 67 | 68 | @JsonPOJOBuilder(withPrefix = StringUtils.EMPTY) 69 | public static final class SavingBuilder { 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/model/notification/Frequency.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.model.notification; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.github.galleog.piggymetrics.core.enums.Enum; 5 | import com.github.galleog.piggymetrics.core.enums.json.EnumDeserializer; 6 | 7 | /** 8 | * Enumeration for notification frequencies. 9 | */ 10 | @JsonDeserialize(using = EnumDeserializer.class) 11 | public class Frequency extends Enum { 12 | /** 13 | * Notifications should be sent weekly. 14 | */ 15 | public static final Frequency WEEKLY = new Frequency(7); 16 | /** 17 | * Notifications should be sent monthly. 18 | */ 19 | public static final Frequency MONTHLY = new Frequency(30); 20 | /** 21 | * Notifications should be sent quarterly. 22 | */ 23 | public static final Frequency QUARTERLY = new Frequency(90); 24 | 25 | private Frequency(int days) { 26 | super(days); 27 | } 28 | 29 | /** 30 | * Gets an enumeration value by the specified key. 31 | * 32 | * @param days the number of days that defines the value 33 | * @throws IllegalArgumentException if the enumeration contains no value with the key {@code days} 34 | */ 35 | public static Frequency valueOf(int days) { 36 | return Enum.valueOf(Frequency.class, days); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/model/notification/NotificationSettings.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.model.notification; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; 5 | import lombok.Builder; 6 | import lombok.Getter; 7 | import org.apache.commons.lang3.StringUtils; 8 | import org.apache.commons.lang3.Validate; 9 | import org.springframework.lang.NonNull; 10 | import org.springframework.lang.Nullable; 11 | 12 | import java.time.LocalDate; 13 | 14 | /** 15 | * Settings for notifications of a particular {@link NotificationType}. 16 | */ 17 | @Getter 18 | @JsonDeserialize(builder = NotificationSettings.NotificationSettingsBuilder.class) 19 | public class NotificationSettings { 20 | /** 21 | * Indicates if the notification is active. Default is {@code true}. 22 | */ 23 | @Builder.Default 24 | private boolean active = true; 25 | /** 26 | * Notification frequency. 27 | */ 28 | @NonNull 29 | private Frequency frequency; 30 | /** 31 | * Date when the notification was last sent. 32 | */ 33 | @Nullable 34 | private LocalDate notifyDate; 35 | 36 | @Builder 37 | @SuppressWarnings("unused") 38 | private NotificationSettings(boolean active, @NonNull Frequency frequency, @Nullable LocalDate notifyDate) { 39 | setActive(active); 40 | setFrequency(frequency); 41 | setNotifyDate(notifyDate); 42 | } 43 | 44 | private void setActive(boolean active) { 45 | this.active = active; 46 | } 47 | 48 | private void setFrequency(Frequency frequency) { 49 | Validate.notNull(frequency); 50 | this.frequency = frequency; 51 | } 52 | 53 | private void setNotifyDate(LocalDate date) { 54 | this.notifyDate = date; 55 | } 56 | 57 | @JsonPOJOBuilder(withPrefix = StringUtils.EMPTY) 58 | public static final class NotificationSettingsBuilder { 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/model/notification/NotificationType.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.model.notification; 2 | 3 | /** 4 | * Enumeration for notification types. 5 | */ 6 | public enum NotificationType { 7 | /** 8 | * Backup notification. 9 | */ 10 | BACKUP, 11 | /** 12 | * Reminder notification. 13 | */ 14 | REMIND 15 | } 16 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/model/notification/Recipient.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.model.notification; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; 5 | import com.google.common.collect.ImmutableMap; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.Singular; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.apache.commons.lang3.Validate; 11 | import org.springframework.lang.NonNull; 12 | import org.springframework.lang.Nullable; 13 | 14 | import java.util.Map; 15 | 16 | /** 17 | * Recipient of notifications. 18 | */ 19 | @Getter 20 | @JsonDeserialize(builder = Recipient.RecipientBuilder.class) 21 | public class Recipient { 22 | /** 23 | * Name of the user to send notifications to. 24 | */ 25 | @Nullable 26 | private String username; 27 | /** 28 | * Email to send notifications to. 29 | */ 30 | @NonNull 31 | private String email; 32 | /** 33 | * Notification settings. 34 | */ 35 | @NonNull 36 | private Map notifications; 37 | 38 | @Builder 39 | @SuppressWarnings("unused") 40 | private Recipient(@Nullable String username, @NonNull String email, 41 | @NonNull @Singular Map notifications) { 42 | setUsername(username); 43 | setEmail(email); 44 | setNotifications(notifications); 45 | } 46 | 47 | private void setUsername(String username) { 48 | this.username = username; 49 | } 50 | 51 | private void setEmail(String email) { 52 | Validate.notNull(email); 53 | this.email = email; 54 | } 55 | 56 | private void setNotifications(Map notifications) { 57 | Validate.notNull(notifications); 58 | Validate.noNullElements(notifications.values()); 59 | this.notifications = ImmutableMap.copyOf(notifications); 60 | } 61 | 62 | @JsonPOJOBuilder(withPrefix = StringUtils.EMPTY) 63 | public static final class RecipientBuilder { 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/model/statistics/ItemMetric.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.model.statistics; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; 5 | import com.github.galleog.piggymetrics.statistics.grpc.StatisticsServiceProto.ItemType; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.apache.commons.lang3.Validate; 10 | import org.apache.commons.lang3.builder.ToStringBuilder; 11 | import org.springframework.lang.NonNull; 12 | 13 | import java.math.BigDecimal; 14 | 15 | /** 16 | * Normalized incomes and expenses with the USD currency and base time period. 17 | */ 18 | @Getter 19 | @JsonDeserialize(builder = ItemMetric.ItemMetricBuilder.class) 20 | public class ItemMetric { 21 | /** 22 | * Item type. 23 | */ 24 | @NonNull 25 | private ItemType type; 26 | /** 27 | * Item title. 28 | */ 29 | @NonNull 30 | private String title; 31 | /** 32 | * Metric monetary amount. 33 | */ 34 | @NonNull 35 | private BigDecimal moneyAmount; 36 | 37 | @Builder 38 | @SuppressWarnings("unused") 39 | private ItemMetric(@NonNull ItemType type, @NonNull String title, @NonNull BigDecimal moneyAmount) { 40 | setType(type); 41 | setTitle(title); 42 | setMoneyAmount(moneyAmount); 43 | } 44 | 45 | @Override 46 | public String toString() { 47 | return new ToStringBuilder(this) 48 | .append("type", getType()) 49 | .append("title", getTitle()) 50 | .build(); 51 | } 52 | 53 | private void setType(ItemType type) { 54 | Validate.notNull(type); 55 | this.type = type; 56 | } 57 | 58 | private void setTitle(String title) { 59 | Validate.notBlank(title); 60 | Validate.isTrue(title.length() <= 20); 61 | this.title = title; 62 | } 63 | 64 | private void setMoneyAmount(BigDecimal moneyAmount) { 65 | Validate.notNull(moneyAmount); 66 | Validate.isTrue(moneyAmount.signum() == 1); 67 | this.moneyAmount = moneyAmount; 68 | } 69 | 70 | @JsonPOJOBuilder(withPrefix = StringUtils.EMPTY) 71 | public static final class ItemMetricBuilder { 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/github/galleog/piggymetrics/apigateway/model/statistics/StatisticalMetric.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.model.statistics; 2 | 3 | /** 4 | * Enumeration for statistical metrics. 5 | */ 6 | public enum StatisticalMetric { 7 | /** 8 | * Total incomes. 9 | */ 10 | INCOMES_AMOUNT, 11 | /** 12 | * Total expenses. 13 | */ 14 | EXPENSES_AMOUNT, 15 | /** 16 | * Savings. 17 | */ 18 | SAVING_AMOUNT 19 | } 20 | -------------------------------------------------------------------------------- /api-gateway/src/main/proto/AccountService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/timestamp.proto"; 4 | import "google/protobuf/wrappers.proto"; 5 | import "protobuf/java/type/BigDecimal.proto"; 6 | import "protobuf/java/type/Money.proto"; 7 | 8 | package piggymetrics.account; 9 | 10 | option java_package = "com.github.galleog.piggymetrics.account.grpc"; 11 | option java_outer_classname = "AccountServiceProto"; 12 | 13 | // Enumeration for item types. 14 | enum ItemType { 15 | INCOME = 0; 16 | EXPENSE = 1; 17 | } 18 | 19 | // Time period values. 20 | enum TimePeriod { 21 | YEAR = 0; 22 | QUARTER = 1; 23 | MONTH = 2; 24 | DAY = 3; 25 | HOUR = 4; 26 | } 27 | 28 | // Income or expense item. 29 | message Item { 30 | // Required. Type of the item. 31 | ItemType type = 1; 32 | // Required. Item title. 33 | string title = 2; 34 | // Required. Monetary amount of this item. 35 | protobuf.java.type.Money money = 3; 36 | // Required. Item period. 37 | TimePeriod period = 4; 38 | // Required. Item icon. 39 | string icon = 5; 40 | } 41 | 42 | // Saving. 43 | message Saving { 44 | // Required. Saving monetary amount 45 | protobuf.java.type.Money money = 1; 46 | // Required. Saving interest. 47 | protobuf.java.type.BigDecimal interest = 2; 48 | // Indicates if the saving is a deposit. Default is false. 49 | bool deposit = 3; 50 | // Indicates if the saving has capitalization. Default is false. 51 | bool capitalization = 4; 52 | } 53 | 54 | // Request to get an account. 55 | message GetAccountRequest { 56 | // Required. The name the found account should have. 57 | string name = 1; 58 | } 59 | 60 | // Account resource of a user. 61 | message Account { 62 | // Required. The name of the user the account belongs to. 63 | string name = 1; 64 | // Account incomes and expenses. 65 | repeated Item items = 2; 66 | // Required. Account savings. 67 | Saving saving = 3; 68 | // Read-only. Date and time when the account was last changed 69 | google.protobuf.Timestamp update_time = 4; 70 | // Additional note. 71 | string note = 5; 72 | } 73 | 74 | // Event sent when an account is updated 75 | message AccountUpdatedEvent { 76 | // Required. Name of the updated account 77 | string account_name = 1; 78 | // Account incomes and expenses. 79 | repeated Item items = 2; 80 | // Required. Account savings. 81 | Saving saving = 3; 82 | // Additional note. 83 | string note = 4; 84 | } 85 | 86 | // Service to work with accounts. 87 | service AccountService { 88 | // Gets an account by its name. 89 | // Possible exception response statuses: 90 | // NOT_FOUND - no account with the requested name is found 91 | rpc GetAccount (GetAccountRequest) returns (Account); 92 | 93 | // Updates the specified account. 94 | // Possible exception response statuses: 95 | // NOT_FOUND - no account with the requested name is found 96 | rpc UpdateAccount (Account) returns (Account); 97 | } -------------------------------------------------------------------------------- /api-gateway/src/main/proto/RecipientService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/type/date.proto"; 4 | 5 | package piggymetrics.notification; 6 | 7 | option java_package = "com.github.galleog.piggymetrics.notification.grpc"; 8 | option java_outer_classname = "RecipientServiceProto"; 9 | 10 | // Settings for notifications of a particular notification type. 11 | message NotificationSettings { 12 | // Indicates if the notification is active. 13 | bool active = 1; 14 | // Required. Notification frequency in days. 15 | int32 frequency = 2; 16 | // Date when the notification was last sent. 17 | google.type.Date notify_date = 3; 18 | } 19 | 20 | // Notification recipient. 21 | message Recipient { 22 | // Required. Name of the user to send notifications to. 23 | string user_name = 1; 24 | // Required. Email to send notifications to. 25 | string email = 2; 26 | // Notification settings. 27 | map notifications = 3; 28 | } 29 | 30 | // Request to get notification settings for a user. 31 | message GetRecipientRequest { 32 | // Required. Name of the user whose notification settings should be found. 33 | string user_name = 1; 34 | } 35 | 36 | // Service to work with notification settings. 37 | service RecipientService { 38 | // Gets notification settings for a user by its name. 39 | // Possible exception response statuses: 40 | // NOT_FOUND - no notification settings for the specified user are found 41 | rpc GetRecipient (GetRecipientRequest) returns (Recipient); 42 | 43 | // Updates notification settings for the specified user. 44 | rpc UpdateRecipient (Recipient) returns (Recipient); 45 | } -------------------------------------------------------------------------------- /api-gateway/src/main/proto/StatisticsService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/type/date.proto"; 4 | import "protobuf/java/type/BigDecimal.proto"; 5 | 6 | package piggymetrics.statistics; 7 | 8 | option java_package = "com.github.galleog.piggymetrics.statistics.grpc"; 9 | option java_outer_classname = "StatisticsServiceProto"; 10 | 11 | // Enumeration for item types. 12 | enum ItemType { 13 | INCOME = 0; 14 | EXPENSE = 1; 15 | } 16 | 17 | // Normalized income or expense with the base currency and time period. 18 | message ItemMetric { 19 | // Required. Type of the item. 20 | ItemType type = 1; 21 | // Required. Item title. 22 | string title = 2; 23 | // Required. Monetary amount of this item. 24 | protobuf.java.type.BigDecimal money_amount = 3; 25 | } 26 | 27 | // Daily time series data point containing the current account state. 28 | message DataPoint { 29 | // Required. Account name this data point is associated with. 30 | string account_name = 1; 31 | // Required. Date of this data point. 32 | google.type.Date date = 2; 33 | // Account incomes and expenses. 34 | repeated ItemMetric metrics = 3; 35 | // Required. Total statistics of incomes, expenses, and savings. 36 | map statistics = 4; 37 | } 38 | 39 | // Request to list data points for an account. 40 | message ListDataPointsRequest { 41 | // Required. Name of the account to list data points for 42 | string account_name = 1; 43 | } 44 | 45 | // Service to get statistics for an account. 46 | service StatisticsService { 47 | // Lists data points for an account. 48 | // Possible exception response statuses: 49 | // NOT_FOUND - no data points for the requested account is found 50 | rpc ListDataPoints (ListDataPointsRequest) returns (stream DataPoint); 51 | } -------------------------------------------------------------------------------- /api-gateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | jmx: 3 | enabled: false 4 | 5 | jackson: 6 | default-property-inclusion: non_null 7 | serialization: 8 | WRITE_DATES_AS_TIMESTAMPS: false 9 | 10 | security: 11 | oauth2: 12 | resourceserver: 13 | jwt: 14 | jwk-set-uri: "http://${KEYCLOAK_HOST:localhost}:${KEYCLOAK_PORT:8080}/auth/realms/piggymetrics/protocol/openid-connect/certs" 15 | 16 | grpc: 17 | client: 18 | account-service: 19 | address: "dns:///${ACCOUNT_SERVICE_HOST:localhost}:${ACCOUNT_SERVICE_PORT:9090}" 20 | negotiationType: PLAINTEXT 21 | 22 | auth-service: 23 | address: "dns:///${AUTH_SERVICE_HOST:localhost}:${AUTH_SERVICE_PORT:9090}" 24 | negotiationType: PLAINTEXT 25 | 26 | notification-service: 27 | address: "dns:///${NOTIFICATION_SERVICE_HOST:localhost}:${NOTIFICATION_SERVICE_PORT:9090}" 28 | negotiationType: PLAINTEXT 29 | 30 | statistics-service: 31 | address: "dns:///${STATISTICS_SERVICE_HOST:localhost}:${STATISTICS_SERVICE_PORT:9090}" 32 | negotiationType: PLAINTEXT 33 | 34 | server: 35 | error: 36 | include-message: always 37 | 38 | management: 39 | endpoint: 40 | health: 41 | probes: 42 | enabled: true 43 | health: 44 | livenessstate: 45 | enabled: true 46 | readinessstate: 47 | enabled: true -------------------------------------------------------------------------------- /api-gateway/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: api-gateway 4 | -------------------------------------------------------------------------------- /api-gateway/src/test/java/com/github/galleog/piggymetrics/apigateway/config/GrpcTestConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.config; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import io.grpc.inprocess.InProcessChannelBuilder; 5 | import net.devh.boot.grpc.client.channelfactory.AbstractChannelFactory; 6 | import net.devh.boot.grpc.client.channelfactory.GrpcChannelFactory; 7 | import net.devh.boot.grpc.client.config.GrpcChannelsProperties; 8 | import net.devh.boot.grpc.client.interceptor.GlobalClientInterceptorRegistry; 9 | import org.springframework.boot.test.context.TestConfiguration; 10 | import org.springframework.context.annotation.Bean; 11 | 12 | /** 13 | * Test configuration for gRPC that issues in-process requests. 14 | */ 15 | @TestConfiguration 16 | public class GrpcTestConfig { 17 | @Bean 18 | public GrpcChannelFactory inProcessChannelFactory(GrpcChannelsProperties properties, 19 | GlobalClientInterceptorRegistry globalClientInterceptorRegistry) { 20 | return new InProcessChannelFactory(properties, globalClientInterceptorRegistry); 21 | } 22 | 23 | private static class InProcessChannelFactory extends AbstractChannelFactory { 24 | InProcessChannelFactory(GrpcChannelsProperties properties, 25 | GlobalClientInterceptorRegistry globalClientInterceptorRegistry) { 26 | super(properties, globalClientInterceptorRegistry, ImmutableList.of()); 27 | } 28 | 29 | @Override 30 | protected InProcessChannelBuilder newChannelBuilder(String name) { 31 | return InProcessChannelBuilder.forName(name) 32 | .directExecutor(); 33 | } 34 | 35 | @Override 36 | protected void configureSecurity(InProcessChannelBuilder builder, String name) { 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /api-gateway/src/test/java/com/github/galleog/piggymetrics/apigateway/handler/BaseRouterTest.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.apigateway.handler; 2 | 3 | import static com.github.galleog.piggymetrics.apigateway.handler.HandlerUtils.USERNAME_CLAIM; 4 | import static org.mockito.Mockito.spy; 5 | 6 | import com.asarkar.grpc.test.GrpcCleanupExtension; 7 | import com.asarkar.grpc.test.Resources; 8 | import com.github.galleog.piggymetrics.apigateway.config.GrpcTestConfig; 9 | import io.grpc.BindableService; 10 | import io.grpc.Server; 11 | import io.grpc.inprocess.InProcessServerBuilder; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; 16 | import org.springframework.boot.test.context.SpringBootTest; 17 | import org.springframework.context.annotation.Import; 18 | import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers; 19 | import org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.JwtMutator; 20 | import org.springframework.test.context.ActiveProfiles; 21 | import org.springframework.test.context.junit.jupiter.SpringExtension; 22 | import org.springframework.test.web.reactive.server.WebTestClient; 23 | 24 | import java.io.IOException; 25 | import java.time.Duration; 26 | 27 | /** 28 | * Base class for routing requests. 29 | */ 30 | @ActiveProfiles("test") 31 | @SpringBootTest 32 | @AutoConfigureWebTestClient 33 | @ExtendWith({ 34 | SpringExtension.class, 35 | MockitoExtension.class, 36 | GrpcCleanupExtension.class 37 | }) 38 | @Import(GrpcTestConfig.class) 39 | class BaseRouterTest { 40 | @Autowired 41 | protected WebTestClient webClient; 42 | 43 | private Resources resources; 44 | 45 | /** 46 | * Creates a spy of a gRPC {@link BindableService} and cleans it up after tests. 47 | * 48 | * @param cls the service class 49 | * @param serviceName the service name 50 | * @param the service type 51 | * @return a spy of the gRPC service 52 | * @throws IOException if unable to bind the service 53 | */ 54 | protected T spyGrpcService(Class cls, String serviceName) throws IOException { 55 | T service = spy(cls); 56 | Server server = InProcessServerBuilder.forName(serviceName) 57 | .directExecutor() 58 | .addService(service) 59 | .build(); 60 | resources.register(server.start(), Duration.ofSeconds(1)); 61 | return service; 62 | } 63 | 64 | /** 65 | * Mocks a JWT token. 66 | */ 67 | protected JwtMutator mockJwt(String userName) { 68 | return SecurityMockServerConfigurers.mockJwt() 69 | .jwt(builder -> builder.claim(USERNAME_CLAIM, userName)); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /api-gateway/src/test/resources/bootstrap-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | cloud: 3 | kubernetes: 4 | enabled: false -------------------------------------------------------------------------------- /api-gateway/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /charts/account-service/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /charts/account-service/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: "1.0-SNAPSHOT" 3 | description: Account Microservice 4 | name: account-service 5 | version: 0.1.0 6 | dependencies: 7 | - name: common 8 | repository: https://charts.bitnami.com/bitnami 9 | version: 2.0.1 10 | -------------------------------------------------------------------------------- /charts/account-service/files/application.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | com.github.galleog.piggymetrics: DEBUG -------------------------------------------------------------------------------- /charts/account-service/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Get the name of the service config map. 4 | */}} 5 | {{- define "account-service.configmap" -}} 6 | {{- printf "%s-config" (include "common.names.fullname" .) -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Get the role to get pods and and it to the default service account. 11 | */}} 12 | {{- define "account-service.role" -}} 13 | {{ default (include "common.names.fullname" .) .Values.role.name }} 14 | {{- end -}} 15 | 16 | {{/* 17 | Get the name of the service account to use 18 | */}} 19 | {{- define "account-service.serviceAccount" -}} 20 | {{- if .Values.serviceAccount.create -}} 21 | {{ default (include "common.names.fullname" .) .Values.serviceAccount.name }} 22 | {{- else -}} 23 | {{ default "default" .Values.serviceAccount.name }} 24 | {{- end -}} 25 | {{- end -}} -------------------------------------------------------------------------------- /charts/account-service/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "account-service.configmap" . }} 5 | labels: 6 | {{- include "common.labels.standard" . | nindent 4 }} 7 | data: 8 | {{- (.Files.Glob "files/application.yml").AsConfig | nindent 2 }} -------------------------------------------------------------------------------- /charts/account-service/templates/role.yml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ include "common.names.fullname" . }} 6 | labels: 7 | {{- include "common.labels.standard" . | nindent 4 }} 8 | rules: 9 | - apiGroups: [""] 10 | resources: ["configmaps"] 11 | verbs: ["get", "list", "watch"] 12 | - apiGroups: [""] 13 | resources: ["pods"] 14 | verbs: ["get"] 15 | {{- end -}} -------------------------------------------------------------------------------- /charts/account-service/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: {{ include "common.names.fullname" . }} 6 | labels: 7 | {{- include "common.labels.standard" . | nindent 4 }} 8 | roleRef: 9 | kind: Role 10 | name: {{ include "common.names.fullname" . }} 11 | apiGroup: rbac.authorization.k8s.io 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "account-service.serviceAccount" . }} 15 | namespace: {{ .Release.Namespace | quote }} 16 | {{- end }} -------------------------------------------------------------------------------- /charts/account-service/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ .Release.Name }}-account-service 5 | labels: 6 | {{- include "common.labels.standard" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - name: grpc 11 | port: {{ .Values.global.pgm.accountService.port }} 12 | targetPort: grpc 13 | protocol: TCP 14 | selector: 15 | {{- include "common.labels.matchLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /charts/account-service/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "account-service.serviceAccount" . }} 6 | labels: 7 | {{- include "common.labels.standard" . | nindent 4 }} 8 | {{- end }} -------------------------------------------------------------------------------- /charts/account-service/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for account-service. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | debug: 8 | enabled: false 9 | 10 | image: 11 | repository: galleog/piggymetrics-account-service 12 | tag: latest 13 | pullPolicy: IfNotPresent 14 | 15 | livenessProbe: 16 | initialDelaySeconds: 180 17 | periodSeconds: 30 18 | timeoutSeconds: 3 19 | failtureThreshold: 10 20 | readinessProbe: 21 | initialDelaySeconds: 10 22 | 23 | service: 24 | type: ClusterIP 25 | 26 | resources: {} 27 | # We usually recommend not to specify default resources and to leave this as a conscious 28 | # choice for the user. This also increases chances charts run on environments with little 29 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 30 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 31 | # limits: 32 | # cpu: 100m 33 | # memory: 128Mi 34 | # requests: 35 | # cpu: 100m 36 | # memory: 128Mi 37 | 38 | nodeSelector: {} 39 | 40 | tolerations: [] 41 | 42 | affinity: {} 43 | 44 | serviceAccount: 45 | create: true 46 | name: "" 47 | 48 | rbac: 49 | create: true 50 | -------------------------------------------------------------------------------- /charts/api-gateway/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /charts/api-gateway/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: "1.0-SNAPSHOT" 3 | description: API Gateway Microservice 4 | name: api-gateway 5 | version: 0.1.0 6 | dependencies: 7 | - name: common 8 | repository: https://charts.bitnami.com/bitnami 9 | version: 2.0.1 10 | -------------------------------------------------------------------------------- /charts/api-gateway/files/application.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | com.github.galleog.piggymetrics: DEBUG -------------------------------------------------------------------------------- /charts/api-gateway/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Get the name of the service config map. 4 | */}} 5 | {{- define "api-gateway.configmap" -}} 6 | {{- printf "%s-config" (include "common.names.fullname" .) -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Get the name of the service account to use 11 | */}} 12 | {{- define "api-gateway.serviceAccount" -}} 13 | {{- if .Values.serviceAccount.create -}} 14 | {{ default (include "common.names.fullname" .) .Values.serviceAccount.name }} 15 | {{- else -}} 16 | {{ default "default" .Values.serviceAccount.name }} 17 | {{- end -}} 18 | {{- end -}} -------------------------------------------------------------------------------- /charts/api-gateway/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "api-gateway.configmap" . }} 5 | labels: 6 | {{- include "common.labels.standard" . | nindent 4 }} 7 | data: 8 | {{- (.Files.Glob "files/application.yml").AsConfig | nindent 2 }} -------------------------------------------------------------------------------- /charts/api-gateway/templates/role.yml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ include "common.names.fullname" . }} 6 | labels: 7 | {{- include "common.labels.standard" . | nindent 4 }} 8 | rules: 9 | - apiGroups: [""] 10 | resources: ["configmaps"] 11 | verbs: ["get", "list", "watch"] 12 | - apiGroups: [""] 13 | resources: ["pods"] 14 | verbs: ["get"] 15 | {{- end -}} -------------------------------------------------------------------------------- /charts/api-gateway/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: {{ include "common.names.fullname" . }} 6 | labels: 7 | {{- include "common.labels.standard" . | nindent 4 }} 8 | roleRef: 9 | kind: Role 10 | name: {{ include "common.names.fullname" . }} 11 | apiGroup: rbac.authorization.k8s.io 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "api-gateway.serviceAccount" . }} 15 | namespace: {{ .Release.Namespace | quote }} 16 | {{- end }} -------------------------------------------------------------------------------- /charts/api-gateway/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ .Release.Name }}-api-gateway 5 | labels: 6 | {{- include "common.labels.standard" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - name: http 11 | port: {{ .Values.global.pgm.gateway.port }} 12 | targetPort: http 13 | protocol: TCP 14 | selector: 15 | {{- include "common.labels.matchLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /charts/api-gateway/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "api-gateway.serviceAccount" . }} 6 | labels: 7 | {{- include "common.labels.standard" . | nindent 4 }} 8 | {{- end }} -------------------------------------------------------------------------------- /charts/api-gateway/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for api-proxy. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | debug: 8 | enabled: false 9 | 10 | image: 11 | repository: galleog/piggymetrics-api-gateway 12 | tag: latest 13 | pullPolicy: IfNotPresent 14 | 15 | livenessProbe: 16 | initialDelaySeconds: 180 17 | periodSeconds: 30 18 | timeoutSeconds: 3 19 | failtureThreshold: 10 20 | readinessProbe: 21 | initialDelaySeconds: 10 22 | 23 | service: 24 | type: ClusterIP 25 | 26 | resources: {} 27 | # We usually recommend not to specify default resources and to leave this as a conscious 28 | # choice for the user. This also increases chances charts run on environments with little 29 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 30 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 31 | # limits: 32 | # cpu: 100m 33 | # memory: 128Mi 34 | # requests: 35 | # cpu: 100m 36 | # memory: 128Mi 37 | 38 | nodeSelector: {} 39 | 40 | tolerations: [] 41 | 42 | affinity: {} 43 | 44 | serviceAccount: 45 | create: true 46 | name: "" 47 | 48 | rbac: 49 | create: true 50 | -------------------------------------------------------------------------------- /charts/global-values.yaml: -------------------------------------------------------------------------------- 1 | global: 2 | postgresql: 3 | configMap: pgm-postgres-host 4 | secret: pgm-postgres-secret 5 | service: 6 | ports: 7 | postgresql: 5432 8 | 9 | kafka: 10 | servicePort: 9092 11 | configMap: pgm-kafka-brokers 12 | 13 | topic: 14 | userEvents: user-events 15 | accountEvents: account-events 16 | 17 | keycloak: 18 | configMap: pgm-keycloak-host 19 | provider: 20 | image: 21 | repository: galleog/piggymetrics-keycloak-provider 22 | tag: latest 23 | pullPolicy: Always 24 | 25 | pgm: 26 | database: 27 | name: pgm 28 | user: pgm 29 | 30 | frontend: 31 | port: 80 32 | 33 | gateway: 34 | port: 8080 35 | 36 | accountService: 37 | port: 9090 38 | database: 39 | schema: account_service 40 | 41 | notificationService: 42 | port: 9090 43 | database: 44 | schema: notification_service 45 | 46 | statisticsService: 47 | port: 9090 48 | database: 49 | schema: statistics_service -------------------------------------------------------------------------------- /charts/notification-service/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /charts/notification-service/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: "1.0-SNAPSHOT" 3 | description: Notification Microservice 4 | name: notification-service 5 | version: 0.1.0 6 | dependencies: 7 | - name: common 8 | repository: https://charts.bitnami.com/bitnami 9 | version: 2.0.1 10 | -------------------------------------------------------------------------------- /charts/notification-service/files/application.yml: -------------------------------------------------------------------------------- 1 | logging: 2 | level: 3 | com.github.galleog.piggymetrics: DEBUG 4 | 5 | remind: 6 | cron: 0 0 0 * * * 7 | email: 8 | 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" 9 | subject: PiggyMetrics reminder 10 | 11 | backup: 12 | cron: 0 0 12 * * * 13 | email: 14 | text: "Howdy, {0}. Your account backup is ready.\r\n\r\nCheers,\r\nPiggyMetrics team" 15 | subject: PiggyMetrics account backup 16 | attachment: backup.json -------------------------------------------------------------------------------- /charts/notification-service/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Get the name of the service config map. 4 | */}} 5 | {{- define "notification-service.configmap" -}} 6 | {{- printf "%s-config" (include "common.names.fullname" .) -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Get the name of the service account to use 11 | */}} 12 | {{- define "notification-service.serviceAccount" -}} 13 | {{- if .Values.serviceAccount.create -}} 14 | {{ default (include "common.names.fullname" .) .Values.serviceAccount.name }} 15 | {{- else -}} 16 | {{ default "default" .Values.serviceAccount.name }} 17 | {{- end -}} 18 | {{- end -}} -------------------------------------------------------------------------------- /charts/notification-service/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: {{ include "notification-service.configmap" . }} 5 | labels: 6 | {{- include "common.labels.standard" . | nindent 4 }} 7 | data: 8 | {{- (.Files.Glob "files/application.yml").AsConfig | nindent 2 }} -------------------------------------------------------------------------------- /charts/notification-service/templates/role.yml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ include "common.names.fullname" . }} 6 | labels: 7 | {{- include "common.labels.standard" . | nindent 4 }} 8 | rules: 9 | - apiGroups: [""] 10 | resources: ["configmaps"] 11 | verbs: ["get", "list", "watch"] 12 | - apiGroups: [""] 13 | resources: ["pods"] 14 | verbs: ["get"] 15 | {{- end -}} -------------------------------------------------------------------------------- /charts/notification-service/templates/rolebinding.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.rbac.create -}} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: {{ include "common.names.fullname" . }} 6 | labels: 7 | {{- include "common.labels.standard" . | nindent 4 }} 8 | roleRef: 9 | kind: Role 10 | name: {{ include "common.names.fullname" . }} 11 | apiGroup: rbac.authorization.k8s.io 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ include "notification-service.serviceAccount" . }} 15 | namespace: {{ .Release.Namespace | quote }} 16 | {{- end }} -------------------------------------------------------------------------------- /charts/notification-service/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ .Release.Name }}-notification-service 5 | labels: 6 | {{- include "common.labels.standard" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - name: grpc 11 | port: {{ .Values.global.pgm.notificationService.port }} 12 | targetPort: grpc 13 | protocol: TCP 14 | selector: 15 | {{- include "common.labels.matchLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /charts/notification-service/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "notification-service.serviceAccount" . }} 6 | labels: 7 | {{- include "common.labels.standard" . | nindent 4 }} 8 | {{- end }} -------------------------------------------------------------------------------- /charts/notification-service/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for notification-service. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | debug: 8 | enabled: false 9 | 10 | image: 11 | repository: galleog/piggymetrics-notification-service 12 | tag: latest 13 | pullPolicy: IfNotPresent 14 | 15 | livenessProbe: 16 | initialDelaySeconds: 180 17 | periodSeconds: 30 18 | timeoutSeconds: 3 19 | failtureThreshold: 10 20 | readinessProbe: 21 | initialDelaySeconds: 10 22 | 23 | service: 24 | type: ClusterIP 25 | 26 | resources: {} 27 | # We usually recommend not to specify default resources and to leave this as a conscious 28 | # choice for the user. This also increases chances charts run on environments with little 29 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 30 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 31 | # limits: 32 | # cpu: 100m 33 | # memory: 128Mi 34 | # requests: 35 | # cpu: 100m 36 | # memory: 128Mi 37 | 38 | nodeSelector: {} 39 | 40 | tolerations: [] 41 | 42 | affinity: {} 43 | 44 | serviceAccount: 45 | create: true 46 | name: "" 47 | 48 | rbac: 49 | create: true 50 | -------------------------------------------------------------------------------- /charts/pgm-dependencies/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /charts/pgm-dependencies/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: "1.0" 3 | name: pgm-dependencies 4 | version: 0.1.0 5 | dependencies: 6 | - name: common 7 | repository: https://charts.bitnami.com/bitnami 8 | version: 2.0.1 9 | - name: postgresql 10 | repository: https://charts.bitnami.com/bitnami 11 | version: 11.8.1 12 | condition: postgresql.enabled 13 | - name: kafka 14 | repository: https://charts.bitnami.com/bitnami 15 | version: 18.3.0 16 | condition: kafka.enabled 17 | - name: keycloakx 18 | repository: https://codecentric.github.io/helm-charts 19 | version: 1.6.0 20 | condition: keycloakx.enabled 21 | -------------------------------------------------------------------------------- /charts/pgm-dependencies/files/keycloak-db.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | psql=( psql -h localhost -U postgres -p "$POSTGRESQL_PORT_NUMBER" --set ON_ERROR_STOP=on ) 4 | 5 | PGPASSWORD="$POSTGRES_PASSWORD" "${psql[@]}" --set pwd="$KEYCLOAK_POSTGRES_PASSWORD" < ClientCall interceptCall(MethodDescriptor method, 17 | CallOptions callOptions, Channel next) { 18 | logger.info("gRPC outgoing call to {}", method.getFullMethodName()); 19 | return next.newCall(method, callOptions); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /grpc-common/src/main/java/com/github/galleog/grpc/interceptor/LogServerInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.grpc.interceptor; 2 | 3 | import io.grpc.Metadata; 4 | import io.grpc.ServerCall; 5 | import io.grpc.ServerCallHandler; 6 | import io.grpc.ServerInterceptor; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | /** 10 | * Slmple logger for gRPC incoming calls before that are dispatched by a {@link ServerCallHandler}. 11 | */ 12 | @Slf4j 13 | public class LogServerInterceptor implements ServerInterceptor { 14 | @Override 15 | public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, 16 | ServerCallHandler next) { 17 | logger.info("gRPC incoming call to {}", call.getMethodDescriptor().getFullMethodName()); 18 | return next.startCall(call, headers); 19 | } 20 | } -------------------------------------------------------------------------------- /grpc-common/src/main/proto/protobuf/java/type/BigDecimal.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "protobuf/java/type/BigInteger.proto"; 4 | 5 | package protobuf.java.type; 6 | 7 | option java_package = "com.github.galleog.protobuf.java.type"; 8 | option java_outer_classname = "BigDecimalProto"; 9 | 10 | // Protobuf analog for java.math.BigDecimal. 11 | message BigDecimal { 12 | int32 scale = 1; 13 | BigInteger int_val = 2; 14 | } -------------------------------------------------------------------------------- /grpc-common/src/main/proto/protobuf/java/type/BigInteger.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package protobuf.java.type; 4 | 5 | option java_package = "com.github.galleog.protobuf.java.type"; 6 | option java_outer_classname = "BigIntegerProto"; 7 | 8 | // Protobuf analog for java.math.BigInteger. 9 | message BigInteger { 10 | bytes value = 1; 11 | } -------------------------------------------------------------------------------- /grpc-common/src/main/proto/protobuf/java/type/Money.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "protobuf/java/type/BigDecimal.proto"; 4 | 5 | package protobuf.java.type; 6 | 7 | option java_package = "com.github.galleog.protobuf.java.type"; 8 | option java_outer_classname = "MoneyProto"; 9 | 10 | // Protobuf analog for org.javamoney.moneta.Money. 11 | message Money { 12 | // Required. The currency code. 13 | string currency_code = 1; 14 | // Required. The monetary amount 15 | BigDecimal amount = 2; 16 | } -------------------------------------------------------------------------------- /grpc-common/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /keycloak-provider/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox:1.31 2 | 3 | MAINTAINER Oleg Galkin 4 | 5 | COPY ./build/libs/*-all.jar /app/keycloak-provider.jar 6 | -------------------------------------------------------------------------------- /keycloak-provider/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | apply plugin: 'com.google.protobuf' 3 | apply plugin: 'com.github.johnrengelman.shadow' 4 | 5 | description = 'keycloak-provider' 6 | 7 | jar { 8 | manifest { 9 | attributes( 10 | 'Implementation-Title': project.name, 11 | 'Implementation-Version': project.version 12 | ) 13 | } 14 | } 15 | 16 | dependencies { 17 | implementation( 18 | 'com.google.protobuf:protobuf-java', 19 | 'org.apache.kafka:kafka-clients', 20 | 'com.github.daniel-shuy:kafka-protobuf-serde' 21 | ) 22 | 23 | compileOnly( 24 | 'org.apache.commons:commons-lang3', 25 | 'com.google.guava:guava', 26 | 'org.jboss.logging:jboss-logging', 27 | 'org.keycloak:keycloak-core', 28 | 'org.keycloak:keycloak-server-spi', 29 | 'org.keycloak:keycloak-server-spi-private' 30 | ) 31 | 32 | testImplementation( 33 | 'org.apache.commons:commons-lang3', 34 | 'com.google.guava:guava', 35 | 'org.jboss.logging:jboss-logging', 36 | 'org.keycloak:keycloak-core', 37 | 'org.keycloak:keycloak-server-spi', 38 | 'org.keycloak:keycloak-server-spi-private', 39 | 'org.assertj:assertj-core', 40 | 'org.junit.jupiter:junit-jupiter', 41 | 'org.mockito:mockito-junit-jupiter' 42 | ) 43 | } 44 | 45 | protobuf { 46 | protoc { 47 | artifact = "com.google.protobuf:protoc:${ver.protobuf}" 48 | } 49 | } 50 | 51 | idea { 52 | module { 53 | sourceDirs += file("$buildDir/generated/source/proto/main/java") 54 | generatedSourceDirs += file("$buildDir/generated/source/proto/main/java") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /keycloak-provider/src/main/java/com/github/galleog/piggymetrics/keycloak/provider/PiggymetricsEventListenerProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.keycloak.provider; 2 | 3 | import com.github.galleog.piggymetrics.auth.grpc.UserRegisteredEventProto.UserRegisteredEvent; 4 | import com.google.common.annotations.VisibleForTesting; 5 | import lombok.RequiredArgsConstructor; 6 | import org.apache.kafka.clients.producer.Producer; 7 | import org.apache.kafka.clients.producer.ProducerRecord; 8 | import org.jboss.logging.Logger; 9 | import org.keycloak.events.Event; 10 | import org.keycloak.events.EventListenerProvider; 11 | import org.keycloak.events.EventType; 12 | import org.keycloak.events.admin.AdminEvent; 13 | 14 | /** 15 | * Event listener that sends a {@link UserRegisteredEvent} when a new user is registered. 16 | */ 17 | @RequiredArgsConstructor 18 | public class PiggymetricsEventListenerProvider implements EventListenerProvider { 19 | @VisibleForTesting 20 | static final String USERNAME_KEY = "username"; 21 | @VisibleForTesting 22 | static final String EMAIL_KEY = "email"; 23 | 24 | private static final Logger logger = Logger.getLogger(PiggymetricsEventListenerProvider.class); 25 | 26 | private final String topic; 27 | private final Producer producer; 28 | 29 | @Override 30 | public void onEvent(Event event) { 31 | if (EventType.REGISTER.equals(event.getType())) { 32 | UserRegisteredEvent ure = UserRegisteredEvent.newBuilder() 33 | .setUserId(event.getUserId()) 34 | .setUserName(event.getDetails().get(USERNAME_KEY)) 35 | .setEmail(event.getDetails().get(EMAIL_KEY)) 36 | .build(); 37 | ProducerRecord record = new ProducerRecord<>(topic, event.getUserId(), ure); 38 | producer.send(record, ((metadata, exception) -> { 39 | if (metadata != null) { 40 | logger.info("Message on registration of user '" + record.key() + "' sent"); 41 | } else { 42 | logger.error("Failed to send message on registration of user '" + record.key() + "'", exception); 43 | } 44 | })); 45 | } 46 | } 47 | 48 | @Override 49 | public void onEvent(AdminEvent event, boolean includeRepresentation) { 50 | } 51 | 52 | @Override 53 | public void close() { 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /keycloak-provider/src/main/java/com/github/galleog/piggymetrics/keycloak/provider/PiggymetricsEventListenerProviderFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.keycloak.provider; 2 | 3 | import com.github.daniel.shuy.kafka.protobuf.serde.KafkaProtobufSerializer; 4 | import com.github.galleog.piggymetrics.auth.grpc.UserRegisteredEventProto.UserRegisteredEvent; 5 | import org.apache.kafka.clients.producer.KafkaProducer; 6 | import org.apache.kafka.clients.producer.Producer; 7 | import org.apache.kafka.clients.producer.ProducerConfig; 8 | import org.apache.kafka.common.serialization.StringSerializer; 9 | import org.keycloak.Config; 10 | import org.keycloak.events.EventListenerProvider; 11 | import org.keycloak.events.EventListenerProviderFactory; 12 | import org.keycloak.models.KeycloakSession; 13 | import org.keycloak.models.KeycloakSessionFactory; 14 | 15 | import java.util.Properties; 16 | 17 | /** 18 | * Keycloak provider factory for {@link PiggymetricsEventListenerProvider}. 19 | */ 20 | public class PiggymetricsEventListenerProviderFactory implements EventListenerProviderFactory { 21 | private static final String PROPERTIES_RESOURCE = "piggymetrics-kafka-producer.properties"; 22 | private static final String ID = "piggymetrics"; 23 | private static final String KAFKA_BROKERS = System.getenv("KAFKA_BROKERS"); 24 | private static final String USER_EVENTS_TOPIC = System.getenv("USER_EVENTS_TOPIC"); 25 | 26 | private Producer producer; 27 | 28 | @Override 29 | public EventListenerProvider create(KeycloakSession session) { 30 | return new PiggymetricsEventListenerProvider(USER_EVENTS_TOPIC, producer); 31 | } 32 | 33 | @Override 34 | public void init(Config.Scope config) { 35 | Properties properties = new PropertiesReader().getProperties(PROPERTIES_RESOURCE); 36 | properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_BROKERS); 37 | producer = new KafkaProducer<>(properties, new StringSerializer(), new KafkaProtobufSerializer<>()); 38 | } 39 | 40 | @Override 41 | public void postInit(KeycloakSessionFactory factory) { 42 | } 43 | 44 | @Override 45 | public void close() { 46 | if (producer != null) { 47 | producer.flush(); 48 | producer.close(); 49 | } 50 | } 51 | 52 | @Override 53 | public String getId() { 54 | return ID; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /keycloak-provider/src/main/java/com/github/galleog/piggymetrics/keycloak/provider/PropertiesReader.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.keycloak.provider; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.apache.commons.lang3.Validate; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.util.Properties; 9 | 10 | /** 11 | * Reader for property files. 12 | */ 13 | public class PropertiesReader { 14 | /** 15 | * Gets all properties from a classpath resource. 16 | * 17 | * @param resource the name of the resource 18 | * @return properties from the resource 19 | */ 20 | public Properties getProperties(String resource) { 21 | Validate.notBlank(resource); 22 | 23 | Properties properties = new Properties(); 24 | try (InputStream input = this.getClass().getResourceAsStream(StringUtils.prependIfMissing(resource, "/"))) { 25 | properties.load(input); 26 | } catch (IOException e) { 27 | throw new IllegalStateException("Failed to load resource " + resource); 28 | } 29 | return properties; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /keycloak-provider/src/main/proto/UserRegisteredEvent.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package piggymetrics.auth; 4 | 5 | option java_package = "com.github.galleog.piggymetrics.auth.grpc"; 6 | option java_outer_classname = "UserRegisteredEventProto"; 7 | 8 | // Event sent when a user is registered. 9 | message UserRegisteredEvent { 10 | // Required. Identifier of the registered user. 11 | string user_id = 1; 12 | // Required. Name of the registered user. 13 | string user_name = 2; 14 | // Required. User email. 15 | string email = 3; 16 | } -------------------------------------------------------------------------------- /keycloak-provider/src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory: -------------------------------------------------------------------------------- 1 | com.github.galleog.piggymetrics.keycloak.provider.PiggymetricsEventListenerProviderFactory -------------------------------------------------------------------------------- /keycloak-provider/src/main/resources/piggymetrics-kafka-producer.properties: -------------------------------------------------------------------------------- 1 | acks=-1 2 | retries=5 3 | enable.idempotence=true -------------------------------------------------------------------------------- /keycloak-provider/src/test/java/com/github/galleog/piggymetrics/keycloak/provider/PropertiesReaderTest.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.keycloak.provider; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.AbstractMap.SimpleEntry; 8 | import java.util.Properties; 9 | 10 | /** 11 | * Tests for {@link PropertiesReader}. 12 | */ 13 | class PropertiesReaderTest { 14 | private static final String TEST_RESOURCE = "test.properties"; 15 | 16 | /** 17 | * Test for {@link PropertiesReader#getProperties(String)}. 18 | */ 19 | @Test 20 | void shouldGetProperties() { 21 | Properties properties = new PropertiesReader().getProperties(TEST_RESOURCE); 22 | assertThat(properties).containsOnly( 23 | new SimpleEntry<>("prop1", "value1"), new SimpleEntry<>("prop2", "value2") 24 | ); 25 | } 26 | } -------------------------------------------------------------------------------- /keycloak-provider/src/test/resources/test.properties: -------------------------------------------------------------------------------- 1 | prop1=value1 2 | prop2=value2 -------------------------------------------------------------------------------- /liquibase-tc/build.gradle: -------------------------------------------------------------------------------- 1 | description = 'liquibase-tc' 2 | 3 | jar { 4 | manifest { 5 | attributes( 6 | 'Implementation-Title': project.name, 7 | 'Implementation-Version': project.version 8 | ) 9 | } 10 | } 11 | 12 | dependencies { 13 | implementation( 14 | 'org.slf4j:slf4j-api', 15 | 'org.liquibase:liquibase-core' 16 | ) 17 | 18 | runtimeOnly( 19 | 'ch.qos.logback:logback-classic', 20 | 'com.mattbertolini:liquibase-slf4j', 21 | 'org.yaml:snakeyaml' 22 | ) 23 | } -------------------------------------------------------------------------------- /liquibase-tc/src/main/java/com/github/galleog/liquibase/tc/LiquibaseUpdater.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.liquibase.tc; 2 | 3 | import liquibase.Liquibase; 4 | import liquibase.database.jvm.JdbcConnection; 5 | import liquibase.exception.LiquibaseException; 6 | import liquibase.resource.FileSystemResourceAccessor; 7 | import liquibase.resource.ResourceAccessor; 8 | import lombok.AccessLevel; 9 | import lombok.NoArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | 12 | import java.io.File; 13 | import java.sql.Connection; 14 | import java.sql.SQLException; 15 | 16 | /** 17 | * Helper class to define a Testcontainers init function. 18 | */ 19 | @Slf4j 20 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 21 | public final class LiquibaseUpdater { 22 | private static final String LIQUIBASE_CHANGELOGFILE = "changeLogFile"; 23 | 24 | /** 25 | * Updates the database using the Liquibase change log defined as a system property. 26 | * 27 | * @param connection a database connection 28 | * @throws LiquibaseException if Liquibase fails 29 | */ 30 | public static void update(Connection connection) throws LiquibaseException, SQLException { 31 | Liquibase liquibase = createLiquibase(connection); 32 | liquibase.update((String) null); 33 | 34 | // Liquibase sets auto commit to false. We need to reset it back because jOOQ requires it 35 | connection.setAutoCommit(true); 36 | } 37 | 38 | private static Liquibase createLiquibase(Connection connection) throws LiquibaseException { 39 | String changeLogFile = System.getProperty(LIQUIBASE_CHANGELOGFILE); 40 | logger.info("Using changelog file: {}", changeLogFile); 41 | ResourceAccessor accessor = new FileSystemResourceAccessor(new File("/")); 42 | return new Liquibase(changeLogFile, accessor, new JdbcConnection(connection)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /liquibase-tc/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.log.fieldName=logger -------------------------------------------------------------------------------- /notification-service/src/main/java/com/github/galleog/piggymetrics/notification/NotificationServiceApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.notification; 2 | 3 | import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 7 | import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; 8 | import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; 9 | import org.springframework.scheduling.annotation.EnableScheduling; 10 | import org.springframework.transaction.annotation.EnableTransactionManagement; 11 | 12 | /** 13 | * Main Spring Boot application class. 14 | */ 15 | @EnableScheduling 16 | @EnableTransactionManagement 17 | @SpringBootApplication(exclude = { 18 | KafkaAutoConfiguration.class, 19 | DataSourceAutoConfiguration.class, 20 | DataSourceTransactionManagerAutoConfiguration.class 21 | }) 22 | @EnableSchedulerLock(defaultLockAtMostFor = "10m") 23 | public class NotificationServiceApplication { 24 | public static void main(String[] args) { 25 | SpringApplication.run(NotificationServiceApplication.class, args); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/github/galleog/piggymetrics/notification/config/GrpcConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.notification.config; 2 | 3 | import com.github.galleog.grpc.interceptor.LogClientInterceptor; 4 | import com.github.galleog.grpc.interceptor.LogServerInterceptor; 5 | import io.grpc.ClientInterceptor; 6 | import io.grpc.ServerInterceptor; 7 | import net.devh.boot.grpc.client.interceptor.GrpcGlobalClientInterceptor; 8 | import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.Profile; 11 | 12 | /** 13 | * Configuration for gRPC. 14 | */ 15 | @Profile("!test") 16 | @Configuration(proxyBeanMethods = false) 17 | public class GrpcConfig { 18 | @GrpcGlobalClientInterceptor 19 | public ClientInterceptor logClientInterceptor() { 20 | return new LogClientInterceptor(); 21 | } 22 | 23 | @GrpcGlobalServerInterceptor 24 | public ServerInterceptor logServerInterceptor() { 25 | return new LogServerInterceptor(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/github/galleog/piggymetrics/notification/config/JooqConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.notification.config; 2 | 3 | import static com.github.galleog.piggymetrics.notification.domain.Public.PUBLIC; 4 | 5 | import com.github.galleog.piggymetrics.autoconfigure.jooq.JooqProperties; 6 | import org.jooq.conf.MappedSchema; 7 | import org.jooq.conf.RenderMapping; 8 | import org.jooq.conf.Settings; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Profile; 12 | 13 | /** 14 | * Configures database schema for jOOQ. 15 | */ 16 | @Profile("!test") 17 | @Configuration(proxyBeanMethods = false) 18 | public class JooqConfig { 19 | @Bean 20 | public Settings settings(JooqProperties properties) { 21 | return new Settings() 22 | .withRenderMapping( 23 | new RenderMapping() 24 | .withSchemata( 25 | new MappedSchema() 26 | .withInput(PUBLIC.getName()) 27 | .withOutput(properties.getSchema()) 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/github/galleog/piggymetrics/notification/config/ReactiveKafkaConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.notification.config; 2 | 3 | import com.github.daniel.shuy.kafka.protobuf.serde.KafkaProtobufDeserializer; 4 | import com.github.galleog.piggymetrics.auth.grpc.UserRegisteredEventProto.UserRegisteredEvent; 5 | import com.github.galleog.piggymetrics.autoconfigure.kafka.ReactiveKafkaReceiverHelper; 6 | import com.github.galleog.piggymetrics.autoconfigure.kafka.ReceiverOptionsCustomizer; 7 | import com.github.galleog.piggymetrics.notification.event.UserRegisteredEventConsumer; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.kafka.core.reactive.ReactiveKafkaConsumerTemplate; 11 | 12 | /** 13 | * Configuration for reactive Kafka. 14 | */ 15 | @Configuration(proxyBeanMethods = false) 16 | public class ReactiveKafkaConfig { 17 | @Bean 18 | ReceiverOptionsCustomizer receiverOptionsCustomizer() { 19 | return options -> options.withValueDeserializer(new KafkaProtobufDeserializer<>(UserRegisteredEvent.parser())); 20 | } 21 | 22 | @Bean 23 | ReactiveKafkaReceiverHelper receiverHelper( 24 | ReactiveKafkaConsumerTemplate consumerTemplate, 25 | UserRegisteredEventConsumer consumer 26 | ) { 27 | return new ReactiveKafkaReceiverHelper<>(consumerTemplate, consumer); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/github/galleog/piggymetrics/notification/config/ScheduledLockConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.notification.config; 2 | 3 | import io.r2dbc.spi.ConnectionFactory; 4 | import net.javacrumbs.shedlock.core.LockProvider; 5 | import net.javacrumbs.shedlock.provider.r2dbc.R2dbcLockProvider; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Profile; 9 | 10 | /** 11 | * Configuration for ShedLock. 12 | */ 13 | @Profile("!test") 14 | @Configuration(proxyBeanMethods = false) 15 | public class ScheduledLockConfig { 16 | @Bean 17 | public LockProvider lockProvider(ConnectionFactory connectionFactory) { 18 | return new R2dbcLockProvider(connectionFactory); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/github/galleog/piggymetrics/notification/domain/Frequency.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.notification.domain; 2 | 3 | import com.github.galleog.piggymetrics.core.enums.Enum; 4 | 5 | /** 6 | * Enumeration for notification frequencies. 7 | */ 8 | public class Frequency extends Enum { 9 | /** 10 | * Notifications should be sent weekly. 11 | */ 12 | public static final Frequency WEEKLY = new Frequency(7); 13 | /** 14 | * Notifications should be sent monthly. 15 | */ 16 | public static final Frequency MONTHLY = new Frequency(30); 17 | /** 18 | * Notifications should be sent quarterly. 19 | */ 20 | public static final Frequency QUARTERLY = new Frequency(90); 21 | 22 | private Frequency(int days) { 23 | super(days); 24 | } 25 | 26 | /** 27 | * Gets an enumeration value by the specified key. 28 | * 29 | * @param days the number of days that defines the value 30 | * @throws IllegalArgumentException if the enumeration contains no value with the key {@code days} 31 | */ 32 | public static Frequency valueOf(int days) { 33 | return Enum.valueOf(Frequency.class, days); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/github/galleog/piggymetrics/notification/domain/NotificationSettings.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.notification.domain; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import org.apache.commons.lang3.Validate; 6 | import org.apache.commons.lang3.builder.ToStringBuilder; 7 | import org.springframework.lang.NonNull; 8 | import org.springframework.lang.Nullable; 9 | 10 | import java.time.LocalDate; 11 | import java.time.format.DateTimeFormatter; 12 | 13 | /** 14 | * Settings for notifications of a particular {@link NotificationType}. 15 | */ 16 | @Getter 17 | public class NotificationSettings { 18 | /** 19 | * Indicates if the notification is active. Default is {@code true}. 20 | */ 21 | @Builder.Default 22 | private boolean active = true; 23 | /** 24 | * Notification frequency. 25 | */ 26 | private Frequency frequency; 27 | /** 28 | * Date when the notification was last sent. 29 | */ 30 | private LocalDate notifyDate; 31 | 32 | @Builder 33 | @SuppressWarnings("unused") 34 | private NotificationSettings(boolean active, @NonNull Frequency frequency, @Nullable LocalDate notifyDate) { 35 | setActive(active); 36 | setFrequency(frequency); 37 | setNotifyDate(notifyDate); 38 | } 39 | 40 | /** 41 | * Indicates if the recipient is notified. 42 | * 43 | * @return {@code true} if the recipient is notified; {@code false} otherwise 44 | */ 45 | public boolean isNotified() { 46 | return this.getNotifyDate() != null; 47 | } 48 | 49 | /** 50 | * Returns new notification settings with the notified date set to the current date. 51 | * 52 | * @throws IllegalArgumentException if the notification settings aren't active 53 | */ 54 | public NotificationSettings markNotified() { 55 | Validate.isTrue(isActive()); 56 | 57 | return NotificationSettings.builder() 58 | .active(true) 59 | .frequency(getFrequency()) 60 | .notifyDate(LocalDate.now()) 61 | .build(); 62 | } 63 | 64 | @Override 65 | public String toString() { 66 | return new ToStringBuilder(this) 67 | .append("active", isActive()) 68 | .append("frequency", getFrequency()) 69 | .append("notifyDate", getNotifyDate() == null ? null : DateTimeFormatter.ISO_DATE.format(getNotifyDate())) 70 | .build(); 71 | } 72 | 73 | private void setActive(boolean active) { 74 | this.active = active; 75 | } 76 | 77 | private void setFrequency(Frequency frequency) { 78 | Validate.notNull(frequency); 79 | this.frequency = frequency; 80 | } 81 | 82 | private void setNotifyDate(LocalDate date) { 83 | this.notifyDate = date; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/github/galleog/piggymetrics/notification/domain/NotificationType.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.notification.domain; 2 | 3 | import lombok.Getter; 4 | import org.apache.commons.lang3.Validate; 5 | import org.springframework.lang.NonNull; 6 | import org.springframework.lang.Nullable; 7 | 8 | /** 9 | * Enumeration for notification types. 10 | */ 11 | @Getter 12 | public enum NotificationType { 13 | /** 14 | * Backup notification. 15 | */ 16 | BACKUP("backup.email.subject", "backup.email.text", "backup.email.attachment"), 17 | /** 18 | * Reminder notification. 19 | */ 20 | REMIND("remind.email.subject", "remind.email.text", null); 21 | 22 | /** 23 | * Property that defines the notification email subject. 24 | */ 25 | private String subject; 26 | /** 27 | * Property that defines the notification email text. 28 | */ 29 | private String text; 30 | /** 31 | * Property that defines an attachment sent with the notification email. 32 | */ 33 | private String attachment; 34 | 35 | NotificationType(@NonNull String subject, @NonNull String text, @Nullable String attachment) { 36 | setSubject(subject); 37 | setText(text); 38 | setAttachment(attachment); 39 | } 40 | 41 | private void setSubject(String subject) { 42 | Validate.notBlank(subject); 43 | this.subject = subject; 44 | } 45 | 46 | private void setText(String text) { 47 | Validate.notBlank(text); 48 | this.text = text; 49 | } 50 | 51 | private void setAttachment(String attachment) { 52 | this.attachment = attachment; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/github/galleog/piggymetrics/notification/event/UserRegisteredEventConsumer.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.notification.event; 2 | 3 | import com.github.galleog.piggymetrics.auth.grpc.UserRegisteredEventProto.UserRegisteredEvent; 4 | import com.github.galleog.piggymetrics.notification.domain.Frequency; 5 | import com.github.galleog.piggymetrics.notification.domain.NotificationSettings; 6 | import com.github.galleog.piggymetrics.notification.domain.NotificationType; 7 | import com.github.galleog.piggymetrics.notification.domain.Recipient; 8 | import com.github.galleog.piggymetrics.notification.repository.RecipientRepository; 9 | import lombok.RequiredArgsConstructor; 10 | import lombok.extern.slf4j.Slf4j; 11 | import org.apache.kafka.clients.consumer.ConsumerRecord; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.transaction.reactive.TransactionalOperator; 14 | import reactor.core.publisher.Flux; 15 | import reactor.core.publisher.Mono; 16 | 17 | import java.util.function.Function; 18 | 19 | /** 20 | * Consumer of events on new user registrations. 21 | */ 22 | @Slf4j 23 | @Component 24 | @RequiredArgsConstructor 25 | public class UserRegisteredEventConsumer implements Function>, Mono> { 26 | private final RecipientRepository recipientRepository; 27 | private final TransactionalOperator operator; 28 | 29 | @Override 30 | public Mono apply(Flux> records) { 31 | return records.map(ConsumerRecord::value) 32 | .doOnNext(event -> logger.info("UserRegisteredEvent for user '{}' received", event.getUserName())) 33 | .flatMap(this::doCreateRecipient) 34 | .then(); 35 | } 36 | 37 | private Mono doCreateRecipient(UserRegisteredEvent event) { 38 | return recipientRepository.getByUsername(event.getUserName()) 39 | .doOnNext(r -> logger.warn("Notification settings for user '{}' already exists", r.getUsername())) 40 | .hasElement() 41 | .filter(b -> !b) 42 | .map(b -> newRecipient(event)) 43 | .flatMap(recipientRepository::save) 44 | .doOnNext(r -> logger.info("Notification settings for user '{}' created", r.getUsername())) 45 | .as(operator::transactional); 46 | } 47 | 48 | private Recipient newRecipient(UserRegisteredEvent event) { 49 | var remind = NotificationSettings.builder() 50 | .active(true) 51 | .frequency(Frequency.MONTHLY) 52 | .build(); 53 | return Recipient.builder() 54 | .username(event.getUserName()) 55 | .email(event.getEmail()) 56 | .notification(NotificationType.REMIND, remind) 57 | .build(); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /notification-service/src/main/java/com/github/galleog/piggymetrics/notification/repository/RecipientRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.notification.repository; 2 | 3 | import com.github.galleog.piggymetrics.notification.domain.NotificationType; 4 | import com.github.galleog.piggymetrics.notification.domain.Recipient; 5 | import org.springframework.lang.NonNull; 6 | import reactor.core.publisher.Flux; 7 | import reactor.core.publisher.Mono; 8 | 9 | import java.time.LocalDate; 10 | 11 | /** 12 | * Repository for {@link Recipient}. 13 | */ 14 | public interface RecipientRepository { 15 | /** 16 | * Gets a recipient by their username. 17 | * 18 | * @param username the recipient username 19 | * @return the recipient with the specified username 20 | */ 21 | Mono getByUsername(@NonNull String username); 22 | 23 | /** 24 | * Saves a recipient. 25 | * 26 | * @param recipient the recipient to save 27 | * @return the saved recipient 28 | */ 29 | Mono save(@NonNull Recipient recipient); 30 | 31 | /** 32 | * Updates a recipient. 33 | * 34 | * @param recipient the recipient to update 35 | * @return the updated recipient 36 | */ 37 | Mono update(@NonNull Recipient recipient); 38 | 39 | /** 40 | * Finds recipients that should be notified by the specified date. 41 | * 42 | * @param type the notification type 43 | * @param date the date where recipients should be notified 44 | * @return the found recipients 45 | */ 46 | Flux readyToNotify(@NonNull NotificationType type, @NonNull LocalDate date); 47 | } 48 | -------------------------------------------------------------------------------- /notification-service/src/main/proto/RecipientService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/type/date.proto"; 4 | 5 | package piggymetrics.notification; 6 | 7 | option java_package = "com.github.galleog.piggymetrics.notification.grpc"; 8 | option java_outer_classname = "RecipientServiceProto"; 9 | 10 | // Settings for notifications of a particular notification type. 11 | message NotificationSettings { 12 | // Indicates if the notification is active. 13 | bool active = 1; 14 | // Required. Notification frequency in days. 15 | int32 frequency = 2; 16 | // Date when the notification was last sent. 17 | google.type.Date notify_date = 3; 18 | } 19 | 20 | // Notification recipient. 21 | message Recipient { 22 | // Required. Name of the user to send notifications to. 23 | string user_name = 1; 24 | // Required. Email to send notifications to. 25 | string email = 2; 26 | // Notification settings. 27 | map notifications = 3; 28 | } 29 | 30 | // Request to get notification settings for a user. 31 | message GetRecipientRequest { 32 | // Required. Name of the user whose notification settings should be found. 33 | string user_name = 1; 34 | } 35 | 36 | // Service to work with notification settings. 37 | service RecipientService { 38 | // Gets notification settings for a user by its name. 39 | // Possible exception response statuses: 40 | // NOT_FOUND - no notification settings for the specified user are found 41 | rpc GetRecipient (GetRecipientRequest) returns (Recipient); 42 | 43 | // Updates notification settings for the specified user. 44 | rpc UpdateRecipient (Recipient) returns (Recipient); 45 | } -------------------------------------------------------------------------------- /notification-service/src/main/proto/UserRegisteredEvent.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package piggymetrics.auth; 4 | 5 | option java_package = "com.github.galleog.piggymetrics.auth.grpc"; 6 | option java_outer_classname = "UserRegisteredEventProto"; 7 | 8 | // Event sent when a user is registered. 9 | message UserRegisteredEvent { 10 | // Required. Identifier of the registered user. 11 | string user_id = 1; 12 | // Required. Name of the registered user. 13 | string user_name = 2; 14 | // Required. User email. 15 | string email = 3; 16 | } -------------------------------------------------------------------------------- /notification-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | r2dbc: 3 | url: r2dbc:pool:postgresql://${DATABASE_HOST:localhost}:${DATABASE_PORT:5432}/${DATABASE_NAME:piggymetrics} 4 | username: ${DATABASE_USER:postgres} 5 | password: ${DATABASE_PASSWORD:secret} 6 | 7 | jmx: 8 | enabled: false 9 | 10 | sql: 11 | init: 12 | mode: never 13 | 14 | jooq: 15 | schema: ${DATABASE_SCHEMA:notification_service} 16 | sql-dialect: postgres 17 | 18 | liquibase: 19 | default-schema: ${DATABASE_SCHEMA:notification_service} 20 | driver-class-name: org.postgresql.Driver 21 | url: jdbc:postgresql://${DATABASE_HOST:localhost}:${DATABASE_PORT:5432}/${DATABASE_NAME:piggymetrics} 22 | user: ${DATABASE_USER:postgres} 23 | password: ${DATABASE_PASSWORD:secret} 24 | 25 | cloud: 26 | kubernetes: 27 | reload: 28 | enabled: true 29 | 30 | kafka: 31 | bootstrap-servers: ${KAFKA_BROKERS:localhost:9092} 32 | consumer: 33 | subscribeTopics: ${USER_EVENTS_TOPIC:user-events} 34 | group-id: notification-service 35 | 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 | 52 | grpc: 53 | server: 54 | port: 9090 55 | 56 | client: 57 | account-service: 58 | address: "dns:///${ACCOUNT_SERVICE_HOST:localhost}:${ACCOUNT_SERVER_PORT:9090}" 59 | negotiationType: PLAINTEXT -------------------------------------------------------------------------------- /notification-service/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: notification-service -------------------------------------------------------------------------------- /notification-service/src/test/resources/application-test.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 | main: 16 | banner-mode: off 17 | 18 | datasource: 19 | type: org.postgresql.ds.PGSimpleDataSource 20 | 21 | liquibase: 22 | default-schema: 23 | 24 | mail: 25 | host: smtp.gmail.com 26 | port: 465 27 | username: test 28 | password: test 29 | 30 | -------------------------------------------------------------------------------- /notification-service/src/test/resources/bootstrap-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | cloud: 3 | kubernetes: 4 | enabled: false -------------------------------------------------------------------------------- /notification-service/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /notification-service/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /pgm-autoconfigure/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | 3 | description = 'pgm-autoconfigure' 4 | 5 | jar { 6 | manifest { 7 | attributes( 8 | 'Implementation-Title': project.name, 9 | 'Implementation-Version': project.version 10 | ) 11 | } 12 | } 13 | 14 | dependencies { 15 | api( 16 | 'org.slf4j:slf4j-api' 17 | ) 18 | 19 | implementation( 20 | 'org.springframework.boot:spring-boot-autoconfigure', 21 | 'org.springframework:spring-r2dbc', 22 | 'org.springframework.kafka:spring-kafka', 23 | 'io.projectreactor.kafka:reactor-kafka', 24 | 'org.jooq:jooq' 25 | ) 26 | 27 | testImplementation( 28 | 'org.springframework.boot:spring-boot-starter-test', 29 | 'org.springframework:spring-jdbc', 30 | 'org.springframework:spring-tx', 31 | 'io.projectreactor:reactor-test', 32 | 'com.ninja-squad:DbSetup', 33 | 'org.assertj:assertj-db', 34 | 'io.r2dbc:r2dbc-h2', 35 | 'org.testcontainers:kafka', 36 | 'org.testcontainers:junit-jupiter', 37 | 'com.google.guava:guava', 38 | 'org.awaitility:awaitility', 39 | 'org.hamcrest:hamcrest' 40 | ) 41 | } -------------------------------------------------------------------------------- /pgm-autoconfigure/src/main/java/com/github/galleog/piggymetrics/autoconfigure/jooq/JooqProperties.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.autoconfigure.jooq; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.jooq.SQLDialect; 6 | import org.springframework.boot.context.properties.ConfigurationProperties; 7 | 8 | /** 9 | * Configuration properties for the JOOQ database library. 10 | */ 11 | @Getter 12 | @Setter 13 | @ConfigurationProperties("spring.jooq") 14 | public class JooqProperties { 15 | private SQLDialect sqlDialect = SQLDialect.DEFAULT; 16 | private String schema; 17 | } 18 | -------------------------------------------------------------------------------- /pgm-autoconfigure/src/main/java/com/github/galleog/piggymetrics/autoconfigure/jooq/R2dbcJooqAutoConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.autoconfigure.jooq; 2 | 3 | import io.r2dbc.spi.ConnectionFactory; 4 | import org.jooq.DSLContext; 5 | import org.jooq.conf.Settings; 6 | import org.springframework.beans.factory.ObjectProvider; 7 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; 8 | import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; 9 | import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; 10 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 11 | import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; 12 | import org.springframework.boot.context.properties.EnableConfigurationProperties; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | import org.springframework.r2dbc.core.DatabaseClient; 16 | 17 | /** 18 | * Auto-configuration for the JOOQ database library using R2DBC {@link ConnectionFactory}. 19 | */ 20 | @Configuration(proxyBeanMethods = false) 21 | @ConditionalOnClass(DSLContext.class) 22 | @ConditionalOnBean(DatabaseClient.class) 23 | @EnableConfigurationProperties(JooqProperties.class) 24 | @AutoConfigureAfter(R2dbcAutoConfiguration.class) 25 | public class R2dbcJooqAutoConfiguration { 26 | @Bean 27 | @ConditionalOnMissingBean 28 | public TransactionAwareJooqWrapper transactionAwareJooqWrapper(DatabaseClient databaseClient, JooqProperties properties, 29 | ObjectProvider settings) { 30 | return new TransactionAwareJooqWrapper(databaseClient, properties.getSqlDialect(), settings.getIfAvailable()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pgm-autoconfigure/src/main/java/com/github/galleog/piggymetrics/autoconfigure/jooq/TransactionAwareJooqWrapper.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.autoconfigure.jooq; 2 | 3 | import io.r2dbc.spi.Connection; 4 | import lombok.RequiredArgsConstructor; 5 | import org.jooq.DSLContext; 6 | import org.jooq.Publisher; 7 | import org.jooq.SQLDialect; 8 | import org.jooq.conf.Settings; 9 | import org.jooq.impl.DSL; 10 | import org.springframework.r2dbc.core.ConnectionAccessor; 11 | import reactor.core.publisher.Flux; 12 | import reactor.core.publisher.Mono; 13 | 14 | import java.util.function.Function; 15 | 16 | /** 17 | * Wrapper for Jooq queries that allows them to participate in Spring transactions. 18 | */ 19 | @RequiredArgsConstructor 20 | public class TransactionAwareJooqWrapper { 21 | private final ConnectionAccessor connectionAccessor; 22 | private final SQLDialect sqlDialect; 23 | private final Settings settings; 24 | 25 | /** 26 | * Executes a Jooq query within a transaction-aware connection. 27 | * 28 | * @param fn the Jooq query that is executed within the connection 29 | * @return the resulting {@link Mono} 30 | */ 31 | public Mono withDSLContext(Function> fn) { 32 | return connectionAccessor.inConnection(con -> Mono.from(fn.apply(createDSLContext(con)))); 33 | } 34 | 35 | /** 36 | * Executes a Jooq query within a transaction-aware connection. 37 | * 38 | * @param fn the Jooq query that is executed within the connection 39 | * @return the resulting {@link Flux} 40 | */ 41 | public Flux withDSLContextMany(Function> fn) { 42 | return connectionAccessor.inConnectionMany(con -> Flux.from(fn.apply(createDSLContext(con)))); 43 | } 44 | 45 | private DSLContext createDSLContext(Connection connection) { 46 | return DSL.using(connection, sqlDialect, settings); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pgm-autoconfigure/src/main/java/com/github/galleog/piggymetrics/autoconfigure/kafka/ReactiveKafkaReceiverHelper.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.autoconfigure.kafka; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.apache.kafka.clients.consumer.ConsumerRecord; 5 | import org.springframework.context.SmartLifecycle; 6 | import org.springframework.kafka.core.reactive.ReactiveKafkaConsumerTemplate; 7 | import reactor.core.Disposable; 8 | import reactor.core.publisher.Flux; 9 | import reactor.core.publisher.Mono; 10 | import reactor.kafka.receiver.KafkaReceiver; 11 | 12 | import java.util.function.Function; 13 | 14 | /** 15 | * Helper for {@link KafkaReceiver}. 16 | */ 17 | @RequiredArgsConstructor 18 | public class ReactiveKafkaReceiverHelper implements SmartLifecycle { 19 | private final ReactiveKafkaConsumerTemplate consumerTemplate; 20 | private final Function>, Mono> consumer; 21 | 22 | private Disposable disposable = null; 23 | 24 | @Override 25 | public synchronized void start() { 26 | if (this.disposable == null || this.disposable.isDisposed()) { 27 | var flux = this.consumerTemplate.receiveAutoAck(); 28 | this.disposable = this.consumer.apply(flux).subscribe(); 29 | } 30 | } 31 | 32 | @Override 33 | public synchronized void stop() { 34 | if (this.disposable != null && !this.disposable.isDisposed()) { 35 | this.disposable.dispose(); 36 | } 37 | } 38 | 39 | @Override 40 | public synchronized boolean isRunning() { 41 | return this.disposable != null && !this.disposable.isDisposed(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pgm-autoconfigure/src/main/java/com/github/galleog/piggymetrics/autoconfigure/kafka/ReceiverOptionsCustomizer.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.autoconfigure.kafka; 2 | 3 | import reactor.kafka.receiver.ReceiverOptions; 4 | 5 | /** 6 | * Allows customizing {@link ReceiverOptions}. 7 | */ 8 | @FunctionalInterface 9 | public interface ReceiverOptionsCustomizer { 10 | /** 11 | * Customizes the given {@link ReceiverOptions}. 12 | * 13 | * @param options the options that should be changed 14 | * @return the customized result 15 | */ 16 | ReceiverOptions customize(ReceiverOptions options); 17 | } 18 | -------------------------------------------------------------------------------- /pgm-autoconfigure/src/main/java/com/github/galleog/piggymetrics/autoconfigure/kafka/SenderOptionsCustomizer.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.autoconfigure.kafka; 2 | 3 | import reactor.kafka.sender.SenderOptions; 4 | 5 | /** 6 | * Allows customizing {@link SenderOptions}. 7 | */ 8 | @FunctionalInterface 9 | public interface SenderOptionsCustomizer { 10 | /** 11 | * Customizes the given {@link SenderOptions}. 12 | * 13 | * @param options the options that should be changed 14 | * @return the customized result 15 | */ 16 | SenderOptions customize(SenderOptions options); 17 | } 18 | -------------------------------------------------------------------------------- /pgm-autoconfigure/src/main/resources/META-INF/spring.factories: -------------------------------------------------------------------------------- 1 | org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ 2 | com.github.galleog.piggymetrics.autoconfigure.jooq.R2dbcJooqAutoConfiguration,\ 3 | com.github.galleog.piggymetrics.autoconfigure.kafka.ReactiveKafkaAutoConfiguration -------------------------------------------------------------------------------- /pgm-autoconfigure/src/test/java/com/github/galleog/piggymetrics/autoconfigure/jooq/R2dbcJooqAutoConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.autoconfigure.jooq; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.boot.autoconfigure.AutoConfigurations; 7 | import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; 8 | import org.springframework.boot.test.context.runner.ApplicationContextRunner; 9 | import org.springframework.r2dbc.core.DatabaseClient; 10 | 11 | /** 12 | * Tests for {@link R2dbcJooqAutoConfiguration}. 13 | */ 14 | class R2dbcJooqAutoConfigurationTest { 15 | private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() 16 | .withConfiguration(AutoConfigurations.of(R2dbcJooqAutoConfiguration.class)); 17 | 18 | /** 19 | * Test for failed auto-configuration without {@link DatabaseClient}. 20 | */ 21 | @Test 22 | void shouldNotCreateTransactionAwareJooqWrapperWhenNoDatabaseClientIsAvailable() { 23 | contextRunner.run(context -> assertThat(context).doesNotHaveBean(TransactionAwareJooqWrapper.class)); 24 | } 25 | 26 | /** 27 | * Test for succeeded auto-configuration. 28 | */ 29 | @Test 30 | void shouldCreateTransactionAwareJooqWrapper() { 31 | contextRunner.withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) 32 | .run(context -> assertThat(context).hasSingleBean(TransactionAwareJooqWrapper.class)); 33 | } 34 | } -------------------------------------------------------------------------------- /pgm-autoconfigure/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | main: 3 | banner-mode: off 4 | 5 | datasource: 6 | generate-unique-name: false 7 | name: testdb 8 | 9 | r2dbc: 10 | generate-unique-name: false 11 | name: testdb 12 | 13 | sql: 14 | init: 15 | mode: always 16 | schema-locations: schema.sql 17 | 18 | kafka: 19 | consumer: 20 | group-id: test 21 | auto-offset-reset: earliest 22 | subscribeTopics: test-topic -------------------------------------------------------------------------------- /pgm-autoconfigure/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pgm-autoconfigure/src/test/resources/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE test ( 2 | name VARCHAR(10) PRIMARY KEY 3 | ); -------------------------------------------------------------------------------- /pgm-core/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java-library' 2 | 3 | description = 'pgm-core' 4 | 5 | jar { 6 | manifest { 7 | attributes( 8 | 'Implementation-Title': project.name, 9 | 'Implementation-Version': project.version 10 | ) 11 | } 12 | } 13 | 14 | dependencies { 15 | implementation( 16 | 'org.springframework.boot:spring-boot-starter-json', 17 | 'org.apache.commons:commons-lang3', 18 | 'com.google.guava:guava' 19 | ) 20 | 21 | testImplementation( 22 | 'org.springframework.boot:spring-boot-starter-test', 23 | 'org.assertj:assertj-core', 24 | 'org.junit.jupiter:junit-jupiter' 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/ColorEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Enumeration type for colors. 7 | */ 8 | public final class ColorEnum extends Enum { 9 | public static final ColorEnum RED = new ColorEnum("Red"); 10 | public static final ColorEnum GREEN = new ColorEnum("Green"); 11 | public static final ColorEnum BLUE = new ColorEnum("Blue"); 12 | 13 | private ColorEnum(String color) { 14 | super(color); 15 | } 16 | 17 | public static ColorEnum valueOf(String color) { 18 | return valueOf(ColorEnum.class, color); 19 | } 20 | 21 | public static List values() { 22 | return values(ColorEnum.class); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/DuplicatedKeyEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums; 2 | 3 | /** 4 | * Enumeration type with a duplicated key. 5 | */ 6 | public final class DuplicatedKeyEnum extends Enum { 7 | public static final DuplicatedKeyEnum RED = new DuplicatedKeyEnum("Red"); 8 | public static final DuplicatedKeyEnum GREEN = new DuplicatedKeyEnum("Green"); 9 | public static final DuplicatedKeyEnum GREENISH = new DuplicatedKeyEnum("Green"); // duplicated key 10 | 11 | private DuplicatedKeyEnum(String color) { 12 | super(color); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/ExtendedGreekLetterEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums; 2 | 3 | /** 4 | * Extension of {@link GreekLetterEnum}. 5 | */ 6 | public final class ExtendedGreekLetterEnum extends GreekLetterEnum { 7 | public static final GreekLetterEnum GAMMA = new ExtendedGreekLetterEnum("Gamma"); 8 | 9 | private ExtendedGreekLetterEnum(String letter) { 10 | super(letter); 11 | } 12 | 13 | public static GreekLetterEnum valueOf(String letter) { 14 | return valueOf(ExtendedGreekLetterEnum.class, letter); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/GreekLetterEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Enumeration type for Greek letters. 7 | */ 8 | public class GreekLetterEnum extends Enum { 9 | public static final GreekLetterEnum ALPHA = new GreekLetterEnum("Alpha"); 10 | public static final GreekLetterEnum BETA = new GreekLetterEnum("Beta"); 11 | 12 | protected GreekLetterEnum(String letter) { 13 | super(letter); 14 | } 15 | 16 | public static GreekLetterEnum valueOf(String letter) { 17 | return valueOf(GreekLetterEnum.class, letter); 18 | } 19 | 20 | public static List values() { 21 | return values(GreekLetterEnum.class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/InvalidClassEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums; 2 | 3 | /** 4 | * Enumeration type with invalid {@link #getEnumClass()}. 5 | */ 6 | public abstract class InvalidClassEnum extends Enum { 7 | public static final InvalidClassEnum PLUS = new InvalidClassEnum("Plus") { 8 | @Override 9 | public int eval(int a, int b) { 10 | return (a + b); 11 | } 12 | }; 13 | 14 | public static final InvalidClassEnum MINUS = new InvalidClassEnum("Minus") { 15 | @Override 16 | public int eval(int a, int b) { 17 | return (a - b); 18 | } 19 | }; 20 | 21 | private InvalidClassEnum(String operation) { 22 | super(operation); 23 | } 24 | 25 | @Override 26 | public Class> getEnumClass() { 27 | return ColorEnum.class; 28 | } 29 | 30 | public abstract int eval(int a, int b); 31 | } 32 | -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/NullClassEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums; 2 | 3 | /** 4 | * Enumeration type with {@code null}nu {@link #getEnumClass()}. 5 | */ 6 | public abstract class NullClassEnum extends Enum { 7 | public static final NullClassEnum PLUS = new NullClassEnum("Plus") { 8 | @Override 9 | public int eval(int a, int b) { 10 | return (a + b); 11 | } 12 | }; 13 | 14 | public static final NullClassEnum MINUS = new NullClassEnum("Minus") { 15 | @Override 16 | public int eval(int a, int b) { 17 | return (a - b); 18 | } 19 | }; 20 | 21 | private NullClassEnum(String operation) { 22 | super(operation); 23 | } 24 | 25 | @Override 26 | public Class> getEnumClass() { 27 | return null; 28 | } 29 | 30 | public abstract int eval(int a, int b); 31 | } 32 | -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/OperationEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Enumeration type for mathematical operations. 7 | */ 8 | public abstract class OperationEnum extends Enum { 9 | public static final OperationEnum PLUS = new OperationEnum("Plus") { 10 | @Override 11 | public int eval(int a, int b) { 12 | return (a + b); 13 | } 14 | }; 15 | 16 | public static final OperationEnum MINUS = new OperationEnum("Minus") { 17 | @Override 18 | public int eval(int a, int b) { 19 | return (a - b); 20 | } 21 | }; 22 | 23 | private OperationEnum(String operation) { 24 | super(operation); 25 | } 26 | 27 | public static OperationEnum valueOf(String operation) { 28 | return valueOf(OperationEnum.class, operation); 29 | } 30 | 31 | public static List values() { 32 | return values(OperationEnum.class); 33 | } 34 | 35 | @Override 36 | public Class> getEnumClass() { 37 | return OperationEnum.class; 38 | } 39 | 40 | public abstract int eval(int a, int b); 41 | } 42 | -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/json/EnumDeserializerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums.json; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | import static org.assertj.core.api.Assertions.assertThatExceptionOfType; 5 | 6 | import com.fasterxml.jackson.databind.JsonMappingException; 7 | import com.fasterxml.jackson.databind.ObjectMapper; 8 | import org.junit.jupiter.api.Test; 9 | 10 | /** 11 | * Tests for {@link EnumDeserializer}. 12 | */ 13 | class EnumDeserializerTest { 14 | private final ObjectMapper objectMapper = new ObjectMapper(); 15 | 16 | /** 17 | * Test for deserialization of non-null values. 18 | */ 19 | @Test 20 | void shouldDeserialize() throws Exception { 21 | TestBean bean = objectMapper.readValue("{\"operation\" : \"Minus\", \"integer\" : 1}", 22 | TestBean.class); 23 | assertThat(bean.getOperation()).isSameAs(OperationEnum.MINUS); 24 | assertThat(bean.getInteger()).isSameAs(IntegerEnum.ONE); 25 | } 26 | 27 | /** 28 | * Test for deserialization of null values. 29 | */ 30 | @Test 31 | void shouldDeserializeNulls() throws Exception { 32 | TestBean bean = objectMapper.readValue("{\"operation\" : null}", TestBean.class); 33 | assertThat(bean.getOperation()).isNull(); 34 | assertThat(bean.getInteger()).isNull(); 35 | } 36 | 37 | /** 38 | * Test for a deserialization exception when the key name is invalid. 39 | */ 40 | @Test 41 | void testDeserializeAbsentKey() { 42 | assertThatExceptionOfType(JsonMappingException.class).isThrownBy(() -> 43 | objectMapper.readValue("{\"integer\" : {\"one\"}}", TestBean.class) 44 | ); 45 | } 46 | 47 | /** 48 | * Test for a deserialization exception when a key value is invalid. 49 | */ 50 | @Test 51 | void testDeserializeInvalidKey() { 52 | assertThatExceptionOfType(JsonMappingException.class).isThrownBy(() -> 53 | objectMapper.readValue("{\"integer\" : 3}", TestBean.class) 54 | ); 55 | } 56 | } -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/json/EnumSerializerTest.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums.json; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Test; 8 | import org.springframework.boot.test.json.JacksonTester; 9 | 10 | /** 11 | * Tests for enum serialization to JSON. 12 | */ 13 | class EnumSerializerTest { 14 | private JacksonTester json; 15 | 16 | @BeforeEach 17 | void setUp() { 18 | ObjectMapper objectMapper = new ObjectMapper(); 19 | JacksonTester.initFields(this, objectMapper); 20 | } 21 | 22 | /** 23 | * Test for serialization of non-null values. 24 | */ 25 | @Test 26 | void shouldSerialize() throws Exception { 27 | TestBean bean = new TestBean(OperationEnum.PLUS, IntegerEnum.TWO); 28 | assertThat(json.write(bean)).extractingJsonPathStringValue("$.operation").isEqualTo("Plus"); 29 | assertThat(json.write(bean)).extractingJsonPathNumberValue("$.integer").isEqualTo(2); 30 | } 31 | 32 | /** 33 | * Test for serialization of null values. 34 | */ 35 | @Test 36 | void shouldSerializeNulls() throws Exception { 37 | TestBean bean = new TestBean(); 38 | assertThat(json.write(bean)).doesNotHaveJsonPathValue("$.operation"); 39 | assertThat(json.write(bean)).doesNotHaveJsonPathValue("$.integer"); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/json/IntegerEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums.json; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.github.galleog.piggymetrics.core.enums.Enum; 5 | 6 | /** 7 | * Enumeration type for integers. 8 | */ 9 | @JsonDeserialize(using = EnumDeserializer.class) 10 | public class IntegerEnum extends Enum { 11 | public static final IntegerEnum ONE = new IntegerEnum(1); 12 | public static final IntegerEnum TWO = new IntegerEnum(2); 13 | 14 | protected IntegerEnum(int key) { 15 | super(key); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/json/OperationEnum.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums.json; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.github.galleog.piggymetrics.core.enums.Enum; 5 | 6 | /** 7 | * Enumeration type for mathematical operations. 8 | */ 9 | @JsonDeserialize(using = EnumDeserializer.class) 10 | public abstract class OperationEnum extends Enum { 11 | public static final OperationEnum PLUS = new OperationEnum("Plus") { 12 | @Override 13 | public int eval(int a, int b) { 14 | return (a + b); 15 | } 16 | }; 17 | 18 | public static final OperationEnum MINUS = new OperationEnum("Minus") { 19 | @Override 20 | public int eval(int a, int b) { 21 | return (a - b); 22 | } 23 | }; 24 | 25 | protected OperationEnum(String operation) { 26 | super(operation); 27 | } 28 | 29 | @Override 30 | public Class> getEnumClass() { 31 | return OperationEnum.class; 32 | } 33 | 34 | public abstract int eval(int a, int b); 35 | } 36 | -------------------------------------------------------------------------------- /pgm-core/src/test/java/com/github/galleog/piggymetrics/core/enums/json/TestBean.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.core.enums.json; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | /** 10 | * Test bean for serialization/deserialization tests. 11 | */ 12 | @Getter 13 | @Setter 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | @JsonInclude(JsonInclude.Include.NON_NULL) 17 | public class TestBean { 18 | private OperationEnum operation; 19 | private IntegerEnum integer; 20 | } 21 | -------------------------------------------------------------------------------- /pgm-core/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /pgm-frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | MAINTAINER Oleg Galkin 4 | 5 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf.template 6 | COPY ./static/ /var/www 7 | 8 | ENV SERVER_PORT 80 9 | ENV SERVER_NAME _ 10 | 11 | CMD ["sh", "-c", "envsubst '$SERVER_PORT $SERVER_NAME' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"] -------------------------------------------------------------------------------- /pgm-frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen $SERVER_PORT; 3 | server_name $SERVER_NAME; 4 | 5 | root /var/www/; 6 | index index.html; 7 | 8 | # Health check endpoint. This will be used by kubernetes to determine if the container is ready/alive. 9 | location = /_healthz { 10 | return 200 'OK'; 11 | } 12 | 13 | # Force all paths to load either itself (js files) or go through index.html 14 | location / { 15 | try_files $uri /index.html; 16 | } 17 | } -------------------------------------------------------------------------------- /pgm-frontend/static/attribution.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Piggy Metrics 7 | 8 | 9 | 10 | 11 | 12 |
13 |
14 |
15 | Creative Commons – Attribution (CC BY 3.0) 16 |
17 | Thanks a lot for icons from The Noun Project collection. 18 |
19 |
20 | Here's the list of all used icons: 21 |
22 | Piggy Bank designed by Jezmael Basilio 23 |
24 | Arrow designed by Jardson A. 25 |
26 | Wallet designed by Luis Prado 27 |
28 | Analytics designed by Aneeque Ahmed 29 |
30 | Piggy Bank designed by Michelle Ann 31 |
32 | Light Bulb designed by Chris Brunskill 33 |
34 | Speech Bubble designed by Cengiz SARI 35 |
36 | Bag designed by Agus Purwanto 37 |
38 | Analytics designed by Luboš Volkov 39 |
40 | College Tuition designed by Rediffusion 41 |
42 | Marijuana designed by Gareth 43 |
44 | Stroller designed by Edward Boatman 45 |
46 | Television designed by Piero Borgo 47 |
48 | Island designed by Bohdan Burmich 49 |
50 | Light Bulb designed by Rémy Médard 51 |
52 | Shirt designed by Megan Sheehan 53 |
54 | Telephone designed by Ian Mawle 55 |
56 | Shopping Cart designed by Megan Sheehan 57 |
58 | Gas designed by Jon Testa 59 |
60 | 61 | -------------------------------------------------------------------------------- /pgm-frontend/static/fonts/museo-100/museo-100.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/fonts/museo-100/museo-100.eot -------------------------------------------------------------------------------- /pgm-frontend/static/fonts/museo-100/museo-100.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/fonts/museo-100/museo-100.ttf -------------------------------------------------------------------------------- /pgm-frontend/static/fonts/museo-100/museo-100.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/fonts/museo-100/museo-100.woff -------------------------------------------------------------------------------- /pgm-frontend/static/fonts/museo-300/museo-300.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/fonts/museo-300/museo-300.eot -------------------------------------------------------------------------------- /pgm-frontend/static/fonts/museo-300/museo-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/fonts/museo-300/museo-300.ttf -------------------------------------------------------------------------------- /pgm-frontend/static/fonts/museo-300/museo-300.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/fonts/museo-300/museo-300.woff -------------------------------------------------------------------------------- /pgm-frontend/static/fonts/museo-500/museo-500.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/fonts/museo-500/museo-500.eot -------------------------------------------------------------------------------- /pgm-frontend/static/fonts/museo-500/museo-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/fonts/museo-500/museo-500.ttf -------------------------------------------------------------------------------- /pgm-frontend/static/fonts/museo-500/museo-500.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/fonts/museo-500/museo-500.woff -------------------------------------------------------------------------------- /pgm-frontend/static/images/1pagesprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/1pagesprites.png -------------------------------------------------------------------------------- /pgm-frontend/static/images/1pagesprites@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/1pagesprites@2x.png -------------------------------------------------------------------------------- /pgm-frontend/static/images/github.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/github.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/icons.png -------------------------------------------------------------------------------- /pgm-frontend/static/images/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/icons@2x.png -------------------------------------------------------------------------------- /pgm-frontend/static/images/linesbackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/linesbackground.png -------------------------------------------------------------------------------- /pgm-frontend/static/images/linesbackground@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/linesbackground@2x.png -------------------------------------------------------------------------------- /pgm-frontend/static/images/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/logo.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/logo@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/logo@2x.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/logo_large.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/logo_large.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/logo_large@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/logo_large@2x.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/logotext.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/logotext.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/logotext@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/logotext@2x.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/logotext_large.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/logotext_large.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/logotext_large@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/logotext_large@2x.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/overview.png -------------------------------------------------------------------------------- /pgm-frontend/static/images/piggy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/piggy.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/piggy@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/piggy@2x.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/piggy_large.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/piggy_large.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/piggy_large@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/piggy_large@2x.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/preloader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/preloader.gif -------------------------------------------------------------------------------- /pgm-frontend/static/images/sprites.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/sprites.png -------------------------------------------------------------------------------- /pgm-frontend/static/images/sprites@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/sprites@2x.png -------------------------------------------------------------------------------- /pgm-frontend/static/images/userpic.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/galleog/piggymetrics-k8s/ff2128a8783c2c4f060522afbfd6d5c24fa7a611/pgm-frontend/static/images/userpic.jpg -------------------------------------------------------------------------------- /pgm-frontend/static/js/launch.js: -------------------------------------------------------------------------------- 1 | var global = { 2 | mobileClient: false, 3 | savePermit: true, 4 | usd: 0, 5 | eur: 0 6 | }; 7 | 8 | $(window).load(function() { 9 | if(/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) ) { 10 | FastClick.attach(document.body); 11 | global.mobileClient = true; 12 | } 13 | 14 | $.getJSON("https://api.exchangerate.host/latest?base=RUB&symbols=EUR,USD", function( data ) { 15 | global.eur = 1 / data.rates.EUR; 16 | global.usd = 1 / data.rates.USD; 17 | }); 18 | }); 19 | 20 | function showGreetingPage(account) { 21 | initAccount(account); 22 | var userAvatar = $("").attr("src","images/userpic.jpg"); 23 | $(userAvatar).load(function() { 24 | setTimeout(initGreetingPage, 500); 25 | }); 26 | } 27 | 28 | function showLoginForm() { 29 | $("#loginpage").show(); 30 | $("#frontloginform").focus(); 31 | setTimeout(initialShaking, 700); 32 | } -------------------------------------------------------------------------------- /pgm-frontend/static/keycloak.json: -------------------------------------------------------------------------------- 1 | { 2 | "realm": "piggymetrics", 3 | "auth-server-url": "/auth/", 4 | "ssl-required": "external", 5 | "resource": "frontend", 6 | "public-client": true, 7 | "confidential-port": 0 8 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | maven { 5 | url 'https://repo.spring.io/plugins-release' 6 | } 7 | } 8 | } 9 | 10 | rootProject.name = 'piggymetrics-k8s' 11 | 12 | include 'pgm-core' 13 | include 'grpc-common' 14 | include 'api-gateway' 15 | include 'account-service' 16 | include 'statistics-service' 17 | include 'notification-service' 18 | include 'liquibase-tc' 19 | include 'keycloak-provider' 20 | include 'pgm-autoconfigure' 21 | 22 | project(':pgm-core').projectDir = "$rootDir/pgm-core" as File 23 | project(':grpc-common').projectDir = "$rootDir/grpc-common" as File 24 | project(':api-gateway').projectDir = "$rootDir/api-gateway" as File 25 | project(':account-service').projectDir = "$rootDir/account-service" as File 26 | project(':statistics-service').projectDir = "$rootDir/statistics-service" as File 27 | project(':notification-service').projectDir = "$rootDir/notification-service" as File 28 | project(':liquibase-tc').projectDir = "$rootDir/liquibase-tc" as File 29 | project(':keycloak-provider').projectDir = "$rootDir/keycloak-provider" as File 30 | project(':pgm-autoconfigure').projectDir = "$rootDir/pgm-autoconfigure" as File -------------------------------------------------------------------------------- /skaffold.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: skaffold/v2beta29 2 | kind: Config 3 | build: 4 | artifacts: 5 | - image: piggymetrics/frontend 6 | context: ./pgm-frontend 7 | - image: piggymetrics/api-gateway 8 | jib: 9 | project: api-gateway 10 | - image: piggymetrics/account-service 11 | jib: 12 | project: account-service 13 | - image: piggymetrics/notification-service 14 | jib: 15 | project: notification-service 16 | - image: piggymetrics/statistics-service 17 | jib: 18 | project: statistics-service 19 | deploy: 20 | helm: 21 | releases: 22 | - name: pgm-dev 23 | chartPath: ./charts/piggymetrics 24 | artifactOverrides: 25 | pgm-frontend.image: piggymetrics/frontend 26 | api-gateway.image: piggymetrics/api-gateway 27 | account-service.image: piggymetrics/account-service 28 | notification-service.image: piggymetrics/notification-service 29 | statistics-service.image: piggymetrics/statistics-service 30 | imageStrategy: 31 | helm: {} 32 | setValues: 33 | pgm-frontend.enabled: false 34 | api-gateway.enabled: false 35 | account-service.enabled: false 36 | notification-service.enabled: false 37 | statistics-service.enabled: true 38 | valuesFiles: 39 | - ./charts/global-values.yaml 40 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/github/galleog/piggymetrics/statistics/StatisticsApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.statistics; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 6 | import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; 7 | import org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration; 8 | import org.springframework.transaction.annotation.EnableTransactionManagement; 9 | 10 | /** 11 | * Main Spring Boot application class. 12 | */ 13 | @EnableTransactionManagement 14 | @SpringBootApplication(exclude = { 15 | KafkaAutoConfiguration.class, 16 | DataSourceAutoConfiguration.class, 17 | DataSourceTransactionManagerAutoConfiguration.class 18 | }) 19 | public class StatisticsApplication { 20 | public static void main(String[] args) { 21 | SpringApplication.run(StatisticsApplication.class, args); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/github/galleog/piggymetrics/statistics/config/GrpcConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.statistics.config; 2 | 3 | import com.github.galleog.grpc.interceptor.LogServerInterceptor; 4 | import io.grpc.ServerInterceptor; 5 | import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | 9 | /** 10 | * Configuration for gRPC. 11 | */ 12 | @Profile("!test") 13 | @Configuration(proxyBeanMethods = false) 14 | public class GrpcConfig { 15 | @GrpcGlobalServerInterceptor 16 | public ServerInterceptor logServerInterceptor() { 17 | return new LogServerInterceptor(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/github/galleog/piggymetrics/statistics/config/JooqConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.statistics.config; 2 | 3 | import static com.github.galleog.piggymetrics.statistics.domain.Public.PUBLIC; 4 | 5 | import com.github.galleog.piggymetrics.autoconfigure.jooq.JooqProperties; 6 | import org.jooq.conf.MappedSchema; 7 | import org.jooq.conf.RenderMapping; 8 | import org.jooq.conf.Settings; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.context.annotation.Profile; 12 | 13 | /** 14 | * Configures database schema for jOOQ. 15 | */ 16 | @Profile("!test") 17 | @Configuration(proxyBeanMethods = false) 18 | public class JooqConfig { 19 | @Bean 20 | public Settings settings(JooqProperties properties) { 21 | return new Settings() 22 | .withRenderMapping( 23 | new RenderMapping() 24 | .withSchemata( 25 | new MappedSchema() 26 | .withInput(PUBLIC.getName()) 27 | .withOutput(properties.getSchema()) 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/github/galleog/piggymetrics/statistics/config/ReactiveKafkaConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.statistics.config; 2 | 3 | import com.github.daniel.shuy.kafka.protobuf.serde.KafkaProtobufDeserializer; 4 | import com.github.galleog.piggymetrics.account.grpc.AccountServiceProto.AccountUpdatedEvent; 5 | import com.github.galleog.piggymetrics.autoconfigure.kafka.ReactiveKafkaReceiverHelper; 6 | import com.github.galleog.piggymetrics.autoconfigure.kafka.ReceiverOptionsCustomizer; 7 | import com.github.galleog.piggymetrics.statistics.event.AccountUpdatedEventConsumer; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.kafka.core.reactive.ReactiveKafkaConsumerTemplate; 11 | 12 | /** 13 | * Configuration for reactive Kafka. 14 | */ 15 | @Configuration(proxyBeanMethods = false) 16 | public class ReactiveKafkaConfig { 17 | @Bean 18 | ReceiverOptionsCustomizer receiverOptionsCustomizer() { 19 | return options -> options.withValueDeserializer(new KafkaProtobufDeserializer<>(AccountUpdatedEvent.parser())); 20 | } 21 | 22 | @Bean 23 | ReactiveKafkaReceiverHelper receiverHelper( 24 | ReactiveKafkaConsumerTemplate consumerTemplate, 25 | AccountUpdatedEventConsumer consumer 26 | ) { 27 | return new ReactiveKafkaReceiverHelper<>(consumerTemplate, consumer); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/github/galleog/piggymetrics/statistics/domain/ItemMetric.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.statistics.domain; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import org.apache.commons.lang3.Validate; 6 | import org.apache.commons.lang3.builder.ToStringBuilder; 7 | import org.springframework.lang.NonNull; 8 | import org.springframework.lang.Nullable; 9 | 10 | import java.math.BigDecimal; 11 | 12 | /** 13 | * Entity for normalized incomes and expenses with the USD currency and 14 | * {@link TimePeriod#getBase()} time period. 15 | */ 16 | @Getter 17 | public class ItemMetric { 18 | /** 19 | * Identifier of this item. 20 | */ 21 | private Long id; 22 | /** 23 | * Item type. 24 | */ 25 | private ItemType type; 26 | /** 27 | * Item title. 28 | */ 29 | private String title; 30 | /** 31 | * Metric monetary amount. 32 | */ 33 | private BigDecimal moneyAmount; 34 | 35 | @Builder 36 | @SuppressWarnings("unused") 37 | private ItemMetric(@Nullable Long id, @NonNull ItemType type, @NonNull String title, @NonNull BigDecimal moneyAmount) { 38 | setId(id); 39 | setType(type); 40 | setTitle(title); 41 | setMoneyAmount(moneyAmount); 42 | } 43 | 44 | @Override 45 | public String toString() { 46 | return new ToStringBuilder(this) 47 | .append("id", getId()) 48 | .append("type", getType()) 49 | .append("title", getTitle()) 50 | .build(); 51 | } 52 | 53 | private void setId(Long id) { 54 | this.id = id; 55 | } 56 | 57 | private void setType(ItemType type) { 58 | Validate.notNull(type); 59 | this.type = type; 60 | } 61 | 62 | private void setTitle(String title) { 63 | Validate.notBlank(title); 64 | Validate.isTrue(title.length() <= 20); 65 | this.title = title; 66 | } 67 | 68 | private void setMoneyAmount(BigDecimal moneyAmount) { 69 | Validate.notNull(moneyAmount); 70 | Validate.isTrue(moneyAmount.signum() == 1); 71 | this.moneyAmount = moneyAmount; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/github/galleog/piggymetrics/statistics/domain/ItemType.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.statistics.domain; 2 | 3 | /** 4 | * Enumeration for item types. 5 | */ 6 | public enum ItemType { 7 | /** 8 | * Item is an income. 9 | */ 10 | INCOME, 11 | /** 12 | * Item is an expense. 13 | */ 14 | EXPENSE 15 | } 16 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/github/galleog/piggymetrics/statistics/domain/StatisticalMetric.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.statistics.domain; 2 | 3 | /** 4 | * Enumeration for statistical metrics. 5 | */ 6 | public enum StatisticalMetric { 7 | /** 8 | * Total incomes. 9 | */ 10 | INCOMES_AMOUNT, 11 | /** 12 | * Total expenses. 13 | */ 14 | EXPENSES_AMOUNT, 15 | /** 16 | * Savings. 17 | */ 18 | SAVING_AMOUNT 19 | } 20 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/github/galleog/piggymetrics/statistics/domain/TimePeriod.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.statistics.domain; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.lang.NonNull; 6 | 7 | /** 8 | * Time period values. 9 | */ 10 | @RequiredArgsConstructor 11 | public enum TimePeriod { 12 | YEAR(365.2425), QUARTER(91.3106), MONTH(30.4368), DAY(1), HOUR(0.0416); 13 | 14 | /** 15 | * Ratio based on the number of days in the time period. 16 | */ 17 | @Getter 18 | private final double baseRatio; 19 | 20 | /** 21 | * Gets the base time period. 22 | */ 23 | @NonNull 24 | public static TimePeriod getBase() { 25 | return DAY; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/github/galleog/piggymetrics/statistics/repository/DataPointRepository.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.statistics.repository; 2 | 3 | import com.github.galleog.piggymetrics.statistics.domain.DataPoint; 4 | import org.springframework.lang.NonNull; 5 | import reactor.core.publisher.Flux; 6 | import reactor.core.publisher.Mono; 7 | 8 | import java.time.LocalDate; 9 | import java.util.Optional; 10 | 11 | /** 12 | * Repository for {@link DataPoint}. 13 | */ 14 | public interface DataPointRepository { 15 | /** 16 | * Gets a data point by an account name and date. 17 | * 18 | * @param accountName the account name 19 | * @param date the data point date 20 | * @return the found data point, or {@link Optional#empty()} 21 | * if there is no data point with the specified account name and date 22 | */ 23 | Mono getByAccountNameAndDate(@NonNull String accountName, @NonNull LocalDate date); 24 | 25 | /** 26 | * Finds all data points associated with the specified account. 27 | * 28 | * @param accountName the account name 29 | * @return the stream of found data points. Clients should ensure the stream is properly closed 30 | */ 31 | Flux listByAccountName(String accountName); 32 | 33 | /** 34 | * Saves a data point. 35 | * 36 | * @param dataPoint the data point to save 37 | * @return the saved data point 38 | */ 39 | Mono save(@NonNull DataPoint dataPoint); 40 | 41 | /** 42 | * Updates a data point. 43 | * 44 | * @param dataPoint the data point to update 45 | * @return the updated data point, or {@link Optional#empty()} 46 | * if there is no data point with the specified account name and date 47 | */ 48 | Mono update(@NonNull DataPoint dataPoint); 49 | } 50 | -------------------------------------------------------------------------------- /statistics-service/src/main/java/com/github/galleog/piggymetrics/statistics/service/MonetaryConversionService.java: -------------------------------------------------------------------------------- 1 | package com.github.galleog.piggymetrics.statistics.service; 2 | 3 | import org.apache.commons.lang3.Validate; 4 | import org.javamoney.moneta.Money; 5 | import org.springframework.stereotype.Service; 6 | 7 | import javax.money.CurrencyUnit; 8 | import javax.money.convert.MonetaryConversions; 9 | 10 | /** 11 | * Service to convert a {@link Money} amount from one currency to another. 12 | */ 13 | @Service 14 | public class MonetaryConversionService { 15 | /** 16 | * Converts a {@link Money} amount from one currency to another. 17 | * 18 | * @param amount the monetary amount to be converted 19 | * @param currency the currency to convert the amount to 20 | * @return the converted monetary amount 21 | * @throws NullPointerException if the amount or the currency to convert to is {@code null} 22 | */ 23 | public Money convert(Money amount, CurrencyUnit currency) { 24 | Validate.notNull(amount); 25 | Validate.notNull(currency); 26 | return amount.with(MonetaryConversions.getConversion(currency)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /statistics-service/src/main/proto/StatisticsService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/type/date.proto"; 4 | import "protobuf/java/type/BigDecimal.proto"; 5 | 6 | package piggymetrics.statistics; 7 | 8 | option java_package = "com.github.galleog.piggymetrics.statistics.grpc"; 9 | option java_outer_classname = "StatisticsServiceProto"; 10 | 11 | // Enumeration for item types. 12 | enum ItemType { 13 | INCOME = 0; 14 | EXPENSE = 1; 15 | } 16 | 17 | // Normalized income or expense with the base currency and time period. 18 | message ItemMetric { 19 | // Required. Type of the item. 20 | ItemType type = 1; 21 | // Required. Item title. 22 | string title = 2; 23 | // Required. Monetary amount of this item. 24 | protobuf.java.type.BigDecimal money_amount = 3; 25 | } 26 | 27 | // Daily time series data point containing the current account state. 28 | message DataPoint { 29 | // Required. Account name this data point is associated with. 30 | string account_name = 1; 31 | // Required. Date of this data point. 32 | google.type.Date date = 2; 33 | // Account incomes and expenses. 34 | repeated ItemMetric metrics = 3; 35 | // Required. Total statistics of incomes, expenses, and savings. 36 | map statistics = 4; 37 | } 38 | 39 | // Request to list data points for an account. 40 | message ListDataPointsRequest { 41 | // Required. Name of the account to list data points for 42 | string account_name = 1; 43 | } 44 | 45 | // Service to get statistics for an account. 46 | service StatisticsService { 47 | // Lists data points for an account. 48 | // Possible exception response statuses: 49 | // NOT_FOUND - no data points for the requested account is found 50 | rpc ListDataPoints (ListDataPointsRequest) returns (stream DataPoint); 51 | } -------------------------------------------------------------------------------- /statistics-service/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | r2dbc: 3 | url: r2dbc:pool:postgresql://${DATABASE_HOST:localhost}:${DATABASE_PORT:5432}/${DATABASE_NAME:piggymetrics} 4 | username: ${DATABASE_USER:postgres} 5 | password: ${DATABASE_PASSWORD:secret} 6 | 7 | jmx: 8 | enabled: false 9 | 10 | jooq: 11 | schema: ${DATABASE_SCHEMA:statistics_service} 12 | sql-dialect: postgres 13 | 14 | liquibase: 15 | default-schema: ${DATABASE_SCHEMA:statistics_service} 16 | driver-class-name: org.postgresql.Driver 17 | url: jdbc:postgresql://${DATABASE_HOST:localhost}:${DATABASE_PORT:5432}/${DATABASE_NAME:piggymetrics} 18 | user: ${DATABASE_USER:postgres} 19 | password: ${DATABASE_PASSWORD:secret} 20 | 21 | kafka: 22 | bootstrap-servers: ${KAFKA_BROKERS:localhost:9092} 23 | consumer: 24 | subscribeTopics: ${ACCOUNT_EVENT_TOPIC:account-events} 25 | group-id: statistics-service 26 | 27 | grpc: 28 | server: 29 | port: 9090 -------------------------------------------------------------------------------- /statistics-service/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: statistics-service -------------------------------------------------------------------------------- /statistics-service/src/test/resources/application-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | main: 3 | banner-mode: off 4 | 5 | datasource: 6 | type: org.postgresql.ds.PGSimpleDataSource 7 | 8 | liquibase: 9 | default-schema: 10 | 11 | grpc: 12 | server: 13 | port: -1 -------------------------------------------------------------------------------- /statistics-service/src/test/resources/bootstrap-test.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | cloud: 3 | kubernetes: 4 | enabled: false -------------------------------------------------------------------------------- /statistics-service/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------