From 5bb46d710d22e3857b0e7c9d3ba09127da5e0d86 Mon Sep 17 00:00:00 2001 From: Wilmer Palomino Date: Wed, 21 Aug 2024 01:11:50 -0500 Subject: [PATCH 1/4] feat: tech challenge solved --- .idea/.gitignore | 8 + .idea/app-java-codechallenge.iml | 9 + .idea/compiler.xml | 21 ++ .idea/encodings.xml | 7 + .idea/jarRepositories.xml | 20 ++ .idea/misc.xml | 15 ++ .idea/modules.xml | 8 + .idea/vcs.xml | 6 + app-antifraud-microservice-kafka/.gitignore | 212 ++++++++++++++++++ app-antifraud-microservice-kafka/pom.xml | 93 ++++++++ .../antifraud/AntifraudApplication.java | 12 + .../antifraud/model/AntifraudResponse.java | 15 ++ .../tecnico/antifraud/model/Transaction.java | 16 ++ .../antifraud/service/AntifraudService.java | 86 +++++++ .../src/main/resources/application.yml | 2 + app-microservice-kafka/.gitignore | 212 ++++++++++++++++++ app-microservice-kafka/pom.xml | 94 ++++++++ .../kafka/KafkaTransactionApplication.java | 12 + .../kafka/model/AntifraudResponse.java | 15 ++ .../reto/tecnico/kafka/model/Transaction.java | 30 +++ .../repository/TransactionRepository.java | 11 + .../kafka/service/TransactionService.java | 105 +++++++++ .../graphql/TransactionGraphQLResolver.java | 33 +++ .../src/main/resources/application.yml | 14 ++ .../main/resources/graphql/schema.graphqls | 14 ++ docker-compose.yml | 11 +- 26 files changed, 1080 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/app-java-codechallenge.iml create mode 100644 .idea/compiler.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 app-antifraud-microservice-kafka/.gitignore create mode 100644 app-antifraud-microservice-kafka/pom.xml create mode 100644 app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/AntifraudApplication.java create mode 100644 app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/AntifraudResponse.java create mode 100644 app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/Transaction.java create mode 100644 app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java create mode 100644 app-antifraud-microservice-kafka/src/main/resources/application.yml create mode 100644 app-microservice-kafka/.gitignore create mode 100644 app-microservice-kafka/pom.xml create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/KafkaTransactionApplication.java create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/AntifraudResponse.java create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/Transaction.java create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionRepository.java create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/graphql/TransactionGraphQLResolver.java create mode 100644 app-microservice-kafka/src/main/resources/application.yml create mode 100644 app-microservice-kafka/src/main/resources/graphql/schema.graphqls diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/app-java-codechallenge.iml b/.idea/app-java-codechallenge.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/app-java-codechallenge.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..750c82b --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..4955411 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b7376dd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..5e7c38b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app-antifraud-microservice-kafka/.gitignore b/app-antifraud-microservice-kafka/.gitignore new file mode 100644 index 0000000..dd13b20 --- /dev/null +++ b/app-antifraud-microservice-kafka/.gitignore @@ -0,0 +1,212 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,intellij+all,eclipse,netbeans,maven,gradle +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,intellij+all,eclipse,netbeans,maven,gradle + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + +### Eclipse Patch ### +# Spring Boot Tooling +.sts4-cache/ + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +### NetBeans ### +**/nbproject/private/ +**/nbproject/Makefile-*.mk +**/nbproject/Package-*.bash +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +### VisualStudioCode ### +.vscode/* +!.vscode/tasks.json +!.vscode/launch.json +*.code-workspace + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Gradle ### +.gradle + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,intellij+all,eclipse,netbeans,maven,gradle diff --git a/app-antifraud-microservice-kafka/pom.xml b/app-antifraud-microservice-kafka/pom.xml new file mode 100644 index 0000000..2c9aa15 --- /dev/null +++ b/app-antifraud-microservice-kafka/pom.xml @@ -0,0 +1,93 @@ + + + 4.0.0 + + org.example + app-antifraud-microservice-kafka + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.7.12 + + + + + 11 + 11 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.kafka + spring-kafka + + + io.projectreactor.kafka + reactor-kafka + 1.3.7 + + + com.fasterxml.jackson.core + jackson-databind + 2.13.3 + + + org.slf4j + slf4j-api + 1.7.36 + + + org.slf4j + slf4j-simple + 1.7.36 + + + org.projectlombok + lombok + 1.18.28 + provided + + + + com.h2database + h2 + runtime + + + + com.graphql-java-kickstart + graphql-spring-boot-starter + 12.0.0 + + + com.graphql-java + graphql-java + + + + + com.graphql-java-kickstart + graphql-java-tools + 12.0.0 + + + com.graphql-java + graphql-java-extended-scalars + 17.0 + + + + \ No newline at end of file diff --git a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/AntifraudApplication.java b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/AntifraudApplication.java new file mode 100644 index 0000000..893caaa --- /dev/null +++ b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/AntifraudApplication.java @@ -0,0 +1,12 @@ +package com.yape.reto.tecnico.antifraud; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AntifraudApplication { + + public static void main(String[] args) { + SpringApplication.run(AntifraudApplication.class, args); + } +} \ No newline at end of file diff --git a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/AntifraudResponse.java b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/AntifraudResponse.java new file mode 100644 index 0000000..c0605d9 --- /dev/null +++ b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/AntifraudResponse.java @@ -0,0 +1,15 @@ +package com.yape.reto.tecnico.antifraud.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class AntifraudResponse { + private Long transactionId; + private boolean isFraudulent; +} \ No newline at end of file diff --git a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/Transaction.java b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/Transaction.java new file mode 100644 index 0000000..2fbfab3 --- /dev/null +++ b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/Transaction.java @@ -0,0 +1,16 @@ +package com.yape.reto.tecnico.antifraud.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Transaction { + private Long id; + private int amount; + private String status; +} diff --git a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java new file mode 100644 index 0000000..1a1263d --- /dev/null +++ b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java @@ -0,0 +1,86 @@ +package com.yape.reto.tecnico.antifraud.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yape.reto.tecnico.antifraud.model.AntifraudResponse; +import com.yape.reto.tecnico.antifraud.model.Transaction; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; +import reactor.kafka.receiver.KafkaReceiver; +import reactor.kafka.receiver.ReceiverOptions; +import reactor.kafka.sender.KafkaSender; +import reactor.kafka.sender.SenderOptions; +import reactor.kafka.sender.SenderRecord; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class AntifraudService { + + private final KafkaReceiver kafkaReceiver; + private final KafkaSender kafkaSender; + private final String inputTopicName = "transaction-created"; + private final String outputTopicName = "antifraud-responses"; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public AntifraudService() { + Map props = new HashMap<>(); + props.put("bootstrap.servers", "localhost:9092"); + props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("auto.offset.reset", "earliest"); + props.put("group.id", "antifraud-group"); + + ReceiverOptions receiverOptions = ReceiverOptions.create(props); + this.kafkaReceiver = KafkaReceiver.create(receiverOptions.subscription(java.util.Collections.singleton(inputTopicName))); + + props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + SenderOptions senderOptions = SenderOptions.create(props); + this.kafkaSender = KafkaSender.create(senderOptions); + + this.kafkaReceiver.receive() + .flatMap(record -> { + Transaction transaction = deserializeTransaction(record.value()); + return analyzeTransaction(transaction) + .then(record.receiverOffset().commit()); + }) + .doOnError(e -> { + System.err.println("Error processing transaction: " + e.getMessage()); + }) + .subscribe(); + } + + private Transaction deserializeTransaction(String json) { + try { + return objectMapper.readValue(json, Transaction.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize transaction", e); + } + } + + private Mono analyzeTransaction(Transaction transaction) { + boolean isFraudulent = transaction.getAmount() > 1000; + + AntifraudResponse response = new AntifraudResponse(); + response.setTransactionId(transaction.getId()); + response.setFraudulent(isFraudulent); + + return publishAntifraudResponse(response); + } + + private Mono publishAntifraudResponse(AntifraudResponse response) { + return Mono.just(response) + .map(r -> { + try { + return objectMapper.writeValueAsString(r); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize antifraud response", e); + } + }) + .flatMap(jsonResponse -> { + SenderRecord senderRecord = SenderRecord.create(outputTopicName, null, null, response.getTransactionId().toString(), jsonResponse, null); + return kafkaSender.send(Mono.just(senderRecord)).then(); + }); + } +} \ No newline at end of file diff --git a/app-antifraud-microservice-kafka/src/main/resources/application.yml b/app-antifraud-microservice-kafka/src/main/resources/application.yml new file mode 100644 index 0000000..58de10c --- /dev/null +++ b/app-antifraud-microservice-kafka/src/main/resources/application.yml @@ -0,0 +1,2 @@ +server: + port: 8090 \ No newline at end of file diff --git a/app-microservice-kafka/.gitignore b/app-microservice-kafka/.gitignore new file mode 100644 index 0000000..dd13b20 --- /dev/null +++ b/app-microservice-kafka/.gitignore @@ -0,0 +1,212 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,intellij+all,eclipse,netbeans,maven,gradle +# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,intellij+all,eclipse,netbeans,maven,gradle + +### Eclipse ### +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Annotation Processing +.apt_generated/ +.apt_generated_test/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet + +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project + +### Eclipse Patch ### +# Spring Boot Tooling +.sts4-cache/ + +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignores the whole .idea folder and all .iml files +# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 + +.idea/ + +# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +# Sonarlint plugin +.idea/sonarlint + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +### NetBeans ### +**/nbproject/private/ +**/nbproject/Makefile-*.mk +**/nbproject/Package-*.bash +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +### VisualStudioCode ### +.vscode/* +!.vscode/tasks.json +!.vscode/launch.json +*.code-workspace + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Gradle ### +.gradle + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### Gradle Patch ### +**/build/ + +# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,intellij+all,eclipse,netbeans,maven,gradle diff --git a/app-microservice-kafka/pom.xml b/app-microservice-kafka/pom.xml new file mode 100644 index 0000000..d6be0c3 --- /dev/null +++ b/app-microservice-kafka/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + org.example + app-microservice-kafka + 1.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.7.12 + + + + + 11 + 11 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.kafka + spring-kafka + + + io.projectreactor.kafka + reactor-kafka + 1.3.7 + + + com.fasterxml.jackson.core + jackson-databind + 2.13.3 + + + org.slf4j + slf4j-api + 1.7.36 + + + org.slf4j + slf4j-simple + 1.7.36 + + + org.projectlombok + lombok + 1.18.28 + provided + + + + com.h2database + h2 + runtime + + + + com.graphql-java-kickstart + graphql-spring-boot-starter + 12.0.0 + + + com.graphql-java + graphql-java + + + + + com.graphql-java-kickstart + graphql-java-tools + 12.0.0 + + + com.graphql-java + graphql-java-extended-scalars + 17.0 + + + + + \ No newline at end of file diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/KafkaTransactionApplication.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/KafkaTransactionApplication.java new file mode 100644 index 0000000..27bef47 --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/KafkaTransactionApplication.java @@ -0,0 +1,12 @@ +package com.yape.reto.tecnico.kafka; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class KafkaTransactionApplication { + + public static void main(String[] args) { + SpringApplication.run(KafkaTransactionApplication.class, args); + } +} \ No newline at end of file diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/AntifraudResponse.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/AntifraudResponse.java new file mode 100644 index 0000000..8e643d8 --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/AntifraudResponse.java @@ -0,0 +1,15 @@ +package com.yape.reto.tecnico.kafka.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class AntifraudResponse { + private Long transactionId; + private boolean isFraudulent; +} diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/Transaction.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/Transaction.java new file mode 100644 index 0000000..34d1c77 --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/Transaction.java @@ -0,0 +1,30 @@ +package com.yape.reto.tecnico.kafka.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; + +@Entity +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class Transaction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private int amount; + private String status; + + public Transaction(int amount, String status) { + this.amount = amount; + this.status = status; + } +} diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionRepository.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionRepository.java new file mode 100644 index 0000000..9a5ddde --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionRepository.java @@ -0,0 +1,11 @@ +package com.yape.reto.tecnico.kafka.repository; + +import com.yape.reto.tecnico.kafka.model.Transaction; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TransactionRepository extends JpaRepository { + + +} diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java new file mode 100644 index 0000000..bc8f07f --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java @@ -0,0 +1,105 @@ +package com.yape.reto.tecnico.kafka.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yape.reto.tecnico.kafka.model.AntifraudResponse; +import com.yape.reto.tecnico.kafka.model.Transaction; +import com.yape.reto.tecnico.kafka.repository.TransactionRepository; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.kafka.receiver.KafkaReceiver; +import reactor.kafka.receiver.ReceiverOptions; +import reactor.kafka.sender.KafkaSender; +import reactor.kafka.sender.SenderOptions; +import reactor.kafka.sender.SenderRecord; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class TransactionService { + + private final TransactionRepository transactionRepository; + private final KafkaSender kafkaSender; + private final KafkaReceiver kafkaReceiver; + private final String topicName = "transaction-created"; + private final ObjectMapper objectMapper = new ObjectMapper(); + + public TransactionService(TransactionRepository transactionRepository) { + this.transactionRepository = transactionRepository; + + Map props = new HashMap<>(); + props.put("bootstrap.servers", "localhost:9092"); + props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); + SenderOptions senderOptions = SenderOptions.create(props); + this.kafkaSender = KafkaSender.create(senderOptions); + + props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); + props.put("group.id", "transaction-group"); + ReceiverOptions receiverOptions = ReceiverOptions.create(props); + this.kafkaReceiver = KafkaReceiver.create(receiverOptions.subscription(java.util.Collections.singleton("antifraud-responses"))); + + this.kafkaReceiver.receive() + .flatMap(record -> { + AntifraudResponse response = deserializeAntifraudResponse(record.value()); + return handleAntifraudResponse(response); + }) + .subscribe(); + } + + private AntifraudResponse deserializeAntifraudResponse(String json) { + try { + return objectMapper.readValue(json, AntifraudResponse.class); + } catch (Exception e) { + throw new RuntimeException("Failed to deserialize antifraud response", e); + } + } + + public Mono createTransaction(int amount) { + Transaction transaction = new Transaction(amount, "PENDING"); + Transaction savedTransaction = transactionRepository.save(transaction); + + return publishTransactionEvent(savedTransaction).thenReturn(savedTransaction); + } + + private Mono handleAntifraudResponse(AntifraudResponse response) { + return Mono.just(response) + .flatMap(r -> { + return Mono.justOrEmpty(transactionRepository.findById(r.getTransactionId())) + .flatMap(transaction -> { + if (r.isFraudulent()) { + transaction.setStatus("REJECTED"); + } else { + transaction.setStatus("APPROVED"); + } + transactionRepository.save(transaction); + return Mono.empty(); + }); + }); + } + + public Mono getTransaction(Long id) { + return Mono.justOrEmpty(transactionRepository.findById(id)); + } + + public Flux getAllTransactions() { + return Flux.fromIterable(transactionRepository.findAll()); + } + + private Mono publishTransactionEvent(Transaction transaction) { + return Mono.just(transaction) + .map(t -> { + try { + return objectMapper.writeValueAsString(t); + } catch (Exception e) { + throw new RuntimeException("Failed to serialize transaction", e); + } + }) + .flatMap(jsonTransaction -> { + SenderRecord senderRecord = SenderRecord.create(topicName, null, null, transaction.getId().toString(), jsonTransaction, null); + return kafkaSender.send(Mono.just(senderRecord)).then(); + }); + } +} diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/graphql/TransactionGraphQLResolver.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/graphql/TransactionGraphQLResolver.java new file mode 100644 index 0000000..ac47461 --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/graphql/TransactionGraphQLResolver.java @@ -0,0 +1,33 @@ +package com.yape.reto.tecnico.kafka.service.graphql; + +import com.yape.reto.tecnico.kafka.model.Transaction; +import com.yape.reto.tecnico.kafka.service.TransactionService; +import org.springframework.stereotype.Component; +import graphql.kickstart.tools.GraphQLQueryResolver; +import graphql.kickstart.tools.GraphQLMutationResolver; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Component +public class TransactionGraphQLResolver implements GraphQLQueryResolver, GraphQLMutationResolver { + + private final TransactionService transactionService; + + public TransactionGraphQLResolver(TransactionService transactionService) { + this.transactionService = transactionService; + } + + public Mono getTransactionById(Long id) { + return transactionService.getTransaction(id); + } + + public Mono> allTransactions() { + return transactionService.getAllTransactions().collectList(); + } + + public Mono createTransaction(int amount) { + return transactionService.createTransaction(amount); + } +} diff --git a/app-microservice-kafka/src/main/resources/application.yml b/app-microservice-kafka/src/main/resources/application.yml new file mode 100644 index 0000000..a4e652f --- /dev/null +++ b/app-microservice-kafka/src/main/resources/application.yml @@ -0,0 +1,14 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver + username: sa + password: password + jpa: + database-platform: org.hibernate.dialect.H2Dialect + h2: + console: + enabled: true + +server: + port: 8080 diff --git a/app-microservice-kafka/src/main/resources/graphql/schema.graphqls b/app-microservice-kafka/src/main/resources/graphql/schema.graphqls new file mode 100644 index 0000000..b9be24a --- /dev/null +++ b/app-microservice-kafka/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,14 @@ +type Transaction { + id: ID! + amount: Int! + status: String +} + +type Query { + transactionById(id: ID!): Transaction + allTransactions: [Transaction!]! +} + +type Mutation { + createTransaction(amount: Int!): Transaction +} diff --git a/docker-compose.yml b/docker-compose.yml index 6e9a9c5..6ac96e3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,4 +22,13 @@ services: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_JMX_PORT: 9991 ports: - - 9092:9092 \ No newline at end of file + - 9092:9092 + kafka-manager: + image: hlebalbau/kafka-manager:stable + ports: + - "9000:9000" + environment: + ZK_HOSTS: zookeeper:2181 + depends_on: + - zookeeper + - kafka \ No newline at end of file From 290a226876877d73c7ba15132999497cbcdce09c Mon Sep 17 00:00:00 2001 From: Wilmer Palomino Date: Wed, 21 Aug 2024 20:34:35 -0500 Subject: [PATCH 2/4] feat: add some request for the challenge --- README.md | 20 ++--- app-antifraud-microservice-kafka/pom.xml | 5 ++ .../antifraud/model/AntifraudResponse.java | 4 +- .../antifraud/model/TransactionMessage.java | 26 +++++++ .../antifraud/model/TransactionStatusDto.java | 19 +++++ ...ansaction.java => TransactionTypeDto.java} | 8 +- .../antifraud/service/AntifraudService.java | 18 +++-- app-microservice-kafka/pom.xml | 5 ++ .../kafka/entity/TransactionStatus.java | 31 ++++++++ .../TransactionType.java} | 17 ++-- .../tecnico/kafka/entity/Transactions.java | 52 +++++++++++++ .../kafka/model/AntifraudResponse.java | 4 +- .../kafka/model/TransactionMessage.java | 26 +++++++ .../kafka/model/TransactionRequest.java | 21 +++++ .../kafka/model/TransactionStatusDto.java | 19 +++++ .../kafka/model/TransactionTypeDto.java | 18 +++++ .../repository/TransactionRepository.java | 6 +- .../TransactionStatusRepository.java | 7 ++ .../repository/TransactionTypeRepository.java | 7 ++ .../kafka/service/TransactionService.java | 78 +++++++++++++++---- .../graphql/TransactionGraphQLResolver.java | 13 ++-- .../src/main/resources/application.yml | 8 ++ .../main/resources/graphql/schema.graphqls | 26 ++++++- .../src/main/resources/query.sql | 6 ++ 24 files changed, 387 insertions(+), 57 deletions(-) create mode 100644 app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/TransactionMessage.java create mode 100644 app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/TransactionStatusDto.java rename app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/{Transaction.java => TransactionTypeDto.java} (67%) create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/entity/TransactionStatus.java rename app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/{model/Transaction.java => entity/TransactionType.java} (57%) create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/entity/Transactions.java create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionMessage.java create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionRequest.java create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionStatusDto.java create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionTypeDto.java create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionStatusRepository.java create mode 100644 app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionTypeRepository.java create mode 100644 app-microservice-kafka/src/main/resources/query.sql diff --git a/README.md b/README.md index 7f832ad..296ffbb 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ Don't forget that the proper way to submit your work is to fork the repo and cre # Problem -Every time a financial transaction is created it must be validated by our anti-fraud microservice and then the same service sends a message back to update the transaction status. -For now, we have only three transaction statuses: +Every time a financial transactions is created it must be validated by our anti-fraud microservice and then the same service sends a message back to update the transactions status. +For now, we have only three transactions statuses:
  1. pending
  2. @@ -21,15 +21,15 @@ For now, we have only three transaction statuses:
  3. rejected
-Every transaction with a value greater than 1000 should be rejected. +Every transactions with a value greater than 1000 should be rejected. ```mermaid flowchart LR Transaction -- Save Transaction with pending Status --> transactionDatabase[(Database)] - Transaction --Send transaction Created event--> Anti-Fraud - Anti-Fraud -- Send transaction Status Approved event--> Transaction - Anti-Fraud -- Send transaction Status Rejected event--> Transaction - Transaction -- Update transaction Status event--> transactionDatabase[(Database)] + Transaction --Send transactions Created event--> Anti-Fraud + Anti-Fraud -- Send transactions Status Approved event--> Transaction + Anti-Fraud -- Send transactions Status Rejected event--> Transaction + Transaction -- Update transactions Status event--> transactionDatabase[(Database)] ``` # Tech Stack @@ -44,7 +44,7 @@ We do provide a `Dockerfile` to help you get started with a dev environment. You must have two resources: -1. Resource to create a transaction that must containt: +1. Resource to create a transactions that must containt: ```json { @@ -55,7 +55,7 @@ You must have two resources: } ``` -2. Resource to retrieve a transaction +2. Resource to retrieve a transactions ```json { @@ -73,7 +73,7 @@ You must have two resources: ## Optional -You can use any approach to store transaction data but you should consider that we may deal with high volume scenarios where we have a huge amount of writes and reads for the same data at the same time. How would you tackle this requirement? +You can use any approach to store transactions data but you should consider that we may deal with high volume scenarios where we have a huge amount of writes and reads for the same data at the same time. How would you tackle this requirement? You can use Graphql; diff --git a/app-antifraud-microservice-kafka/pom.xml b/app-antifraud-microservice-kafka/pom.xml index 2c9aa15..b86017c 100644 --- a/app-antifraud-microservice-kafka/pom.xml +++ b/app-antifraud-microservice-kafka/pom.xml @@ -22,6 +22,11 @@ + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.14.0 + org.springframework.boot spring-boot-starter-webflux diff --git a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/AntifraudResponse.java b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/AntifraudResponse.java index c0605d9..96e2151 100644 --- a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/AntifraudResponse.java +++ b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/AntifraudResponse.java @@ -5,11 +5,13 @@ import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.UUID; + @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class AntifraudResponse { - private Long transactionId; + private UUID transactionId; private boolean isFraudulent; } \ No newline at end of file diff --git a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/TransactionMessage.java b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/TransactionMessage.java new file mode 100644 index 0000000..ac886dd --- /dev/null +++ b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/TransactionMessage.java @@ -0,0 +1,26 @@ +package com.yape.reto.tecnico.antifraud.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public class TransactionMessage { + + private UUID transactionExternalId; + private TransactionTypeDto transactionType; + private TransactionStatusDto transactionStatus; + private Double amount; + private LocalDateTime createdAt; + +} diff --git a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/TransactionStatusDto.java b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/TransactionStatusDto.java new file mode 100644 index 0000000..8de6ca7 --- /dev/null +++ b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/TransactionStatusDto.java @@ -0,0 +1,19 @@ +package com.yape.reto.tecnico.antifraud.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public class TransactionStatusDto { + + private Long id; + private String name; + +} diff --git a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/Transaction.java b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/TransactionTypeDto.java similarity index 67% rename from app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/Transaction.java rename to app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/TransactionTypeDto.java index 2fbfab3..c589ea6 100644 --- a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/Transaction.java +++ b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/model/TransactionTypeDto.java @@ -4,13 +4,15 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; @Getter @Setter @AllArgsConstructor @NoArgsConstructor -public class Transaction { +@SuperBuilder +public class TransactionTypeDto { private Long id; - private int amount; - private String status; + private String name; + } diff --git a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java index 1a1263d..8d96b42 100644 --- a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java +++ b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java @@ -1,8 +1,9 @@ package com.yape.reto.tecnico.antifraud.service; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.yape.reto.tecnico.antifraud.model.AntifraudResponse; -import com.yape.reto.tecnico.antifraud.model.Transaction; +import com.yape.reto.tecnico.antifraud.model.TransactionMessage; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; import reactor.kafka.receiver.KafkaReceiver; @@ -24,6 +25,7 @@ public class AntifraudService { private final ObjectMapper objectMapper = new ObjectMapper(); public AntifraudService() { + objectMapper.registerModule(new JavaTimeModule()); Map props = new HashMap<>(); props.put("bootstrap.servers", "localhost:9092"); props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); @@ -41,8 +43,8 @@ public AntifraudService() { this.kafkaReceiver.receive() .flatMap(record -> { - Transaction transaction = deserializeTransaction(record.value()); - return analyzeTransaction(transaction) + TransactionMessage transactionMessage = deserializeTransaction(record.value()); + return analyzeTransaction(transactionMessage) .then(record.receiverOffset().commit()); }) .doOnError(e -> { @@ -51,19 +53,19 @@ public AntifraudService() { .subscribe(); } - private Transaction deserializeTransaction(String json) { + private TransactionMessage deserializeTransaction(String json) { try { - return objectMapper.readValue(json, Transaction.class); + return objectMapper.readValue(json, TransactionMessage.class); } catch (Exception e) { throw new RuntimeException("Failed to deserialize transaction", e); } } - private Mono analyzeTransaction(Transaction transaction) { - boolean isFraudulent = transaction.getAmount() > 1000; + private Mono analyzeTransaction(TransactionMessage transactions) { + boolean isFraudulent = transactions.getAmount() > 1000; AntifraudResponse response = new AntifraudResponse(); - response.setTransactionId(transaction.getId()); + response.setTransactionId(transactions.getTransactionExternalId()); response.setFraudulent(isFraudulent); return publishAntifraudResponse(response); diff --git a/app-microservice-kafka/pom.xml b/app-microservice-kafka/pom.xml index d6be0c3..d612ca4 100644 --- a/app-microservice-kafka/pom.xml +++ b/app-microservice-kafka/pom.xml @@ -22,6 +22,11 @@ + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.14.0 + org.springframework.boot spring-boot-starter-webflux diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/entity/TransactionStatus.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/entity/TransactionStatus.java new file mode 100644 index 0000000..281f6f3 --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/entity/TransactionStatus.java @@ -0,0 +1,31 @@ +package com.yape.reto.tecnico.kafka.entity; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.OneToOne; +import javax.persistence.Table; + +@Entity +@Table(name = "transaction_status") +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public class TransactionStatus { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String name; + +} diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/Transaction.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/entity/TransactionType.java similarity index 57% rename from app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/Transaction.java rename to app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/entity/TransactionType.java index 34d1c77..a15e249 100644 --- a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/Transaction.java +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/entity/TransactionType.java @@ -1,30 +1,31 @@ -package com.yape.reto.tecnico.kafka.model; +package com.yape.reto.tecnico.kafka.entity; +import com.fasterxml.jackson.annotation.JsonIgnore; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.experimental.SuperBuilder; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; +import javax.persistence.OneToOne; +import javax.persistence.Table; @Entity +@Table(name = "TRANSACTION_TYPE") @Getter @Setter @AllArgsConstructor @NoArgsConstructor -public class Transaction { +@SuperBuilder +public class TransactionType { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - private int amount; - private String status; + private String name; - public Transaction(int amount, String status) { - this.amount = amount; - this.status = status; - } } diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/entity/Transactions.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/entity/Transactions.java new file mode 100644 index 0000000..743d60c --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/entity/Transactions.java @@ -0,0 +1,52 @@ +package com.yape.reto.tecnico.kafka.entity; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; +import org.hibernate.annotations.GenericGenerator; +import org.hibernate.annotations.Type; + +import javax.persistence.CascadeType; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import java.util.UUID; + +@Entity +@Getter +@Table(name = "transactions") +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public class Transactions { + + @Id + @GeneratedValue(generator = "UUID") + @GenericGenerator( + name = "UUID", + strategy = "org.hibernate.id.UUIDGenerator" + ) + @Type(type = "org.hibernate.type.UUIDCharType") + private UUID id; + private String accountExternalIdDebit; + private String accountExternalIdCredit; + private Double amount; + + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinColumn(name = "transaction_type_id", referencedColumnName = "id") + private TransactionType transactionType; + + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinColumn(name = "transaction_status_id", referencedColumnName = "id") + private TransactionStatus transactionStatus; + +} diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/AntifraudResponse.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/AntifraudResponse.java index 8e643d8..04cf2e3 100644 --- a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/AntifraudResponse.java +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/AntifraudResponse.java @@ -5,11 +5,13 @@ import lombok.NoArgsConstructor; import lombok.Setter; +import java.util.UUID; + @Getter @Setter @AllArgsConstructor @NoArgsConstructor public class AntifraudResponse { - private Long transactionId; + private UUID transactionId; private boolean isFraudulent; } diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionMessage.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionMessage.java new file mode 100644 index 0000000..a40b260 --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionMessage.java @@ -0,0 +1,26 @@ +package com.yape.reto.tecnico.kafka.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public class TransactionMessage { + + private UUID transactionExternalId; + private TransactionTypeDto transactionType; + private TransactionStatusDto transactionStatus; + private Double amount; + private LocalDateTime createdAt; + +} diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionRequest.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionRequest.java new file mode 100644 index 0000000..3156a17 --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionRequest.java @@ -0,0 +1,21 @@ +package com.yape.reto.tecnico.kafka.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public class TransactionRequest { + + private String accountExternalIdDebit ; + private String accountExternalIdCredit; + private Long tranferTypeId ; + private Double amount; + +} diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionStatusDto.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionStatusDto.java new file mode 100644 index 0000000..d8e3a5f --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionStatusDto.java @@ -0,0 +1,19 @@ +package com.yape.reto.tecnico.kafka.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public class TransactionStatusDto { + + private Long id; + private String name; + +} diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionTypeDto.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionTypeDto.java new file mode 100644 index 0000000..13e64ef --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/model/TransactionTypeDto.java @@ -0,0 +1,18 @@ +package com.yape.reto.tecnico.kafka.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.experimental.SuperBuilder; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +@SuperBuilder +public class TransactionTypeDto { + private Long id; + private String name; + +} diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionRepository.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionRepository.java index 9a5ddde..3e75986 100644 --- a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionRepository.java +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionRepository.java @@ -1,11 +1,13 @@ package com.yape.reto.tecnico.kafka.repository; -import com.yape.reto.tecnico.kafka.model.Transaction; +import com.yape.reto.tecnico.kafka.entity.Transactions; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.UUID; + @Repository -public interface TransactionRepository extends JpaRepository { +public interface TransactionRepository extends JpaRepository { } diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionStatusRepository.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionStatusRepository.java new file mode 100644 index 0000000..a836624 --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionStatusRepository.java @@ -0,0 +1,7 @@ +package com.yape.reto.tecnico.kafka.repository; + +import com.yape.reto.tecnico.kafka.entity.TransactionStatus; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TransactionStatusRepository extends JpaRepository { +} \ No newline at end of file diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionTypeRepository.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionTypeRepository.java new file mode 100644 index 0000000..11e7fb9 --- /dev/null +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/repository/TransactionTypeRepository.java @@ -0,0 +1,7 @@ +package com.yape.reto.tecnico.kafka.repository; + +import com.yape.reto.tecnico.kafka.entity.TransactionType; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TransactionTypeRepository extends JpaRepository { +} \ No newline at end of file diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java index bc8f07f..1230f67 100644 --- a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java @@ -1,9 +1,20 @@ package com.yape.reto.tecnico.kafka.service; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.yape.reto.tecnico.kafka.entity.Transactions; import com.yape.reto.tecnico.kafka.model.AntifraudResponse; -import com.yape.reto.tecnico.kafka.model.Transaction; +import com.yape.reto.tecnico.kafka.model.TransactionMessage; +import com.yape.reto.tecnico.kafka.model.TransactionRequest; +import com.yape.reto.tecnico.kafka.entity.TransactionStatus; +import com.yape.reto.tecnico.kafka.entity.TransactionType; +import com.yape.reto.tecnico.kafka.model.TransactionStatusDto; +import com.yape.reto.tecnico.kafka.model.TransactionTypeDto; import com.yape.reto.tecnico.kafka.repository.TransactionRepository; +import com.yape.reto.tecnico.kafka.repository.TransactionStatusRepository; +import com.yape.reto.tecnico.kafka.repository.TransactionTypeRepository; +import lombok.extern.log4j.Log4j2; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -13,20 +24,32 @@ import reactor.kafka.sender.SenderOptions; import reactor.kafka.sender.SenderRecord; +import javax.transaction.Transactional; +import java.time.LocalDateTime; +import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.UUID; +@Log4j2 @Service public class TransactionService { private final TransactionRepository transactionRepository; + private final TransactionStatusRepository transactionStatusRepository; + private final TransactionTypeRepository transactionTypeRepository; private final KafkaSender kafkaSender; private final KafkaReceiver kafkaReceiver; private final String topicName = "transaction-created"; private final ObjectMapper objectMapper = new ObjectMapper(); - public TransactionService(TransactionRepository transactionRepository) { + + public TransactionService(TransactionRepository transactionRepository, TransactionStatusRepository transactionStatusRepository, + TransactionTypeRepository transactionTypeRepository) { this.transactionRepository = transactionRepository; + this.transactionStatusRepository = transactionStatusRepository; + this.transactionTypeRepository = transactionTypeRepository; + objectMapper.registerModule(new JavaTimeModule()); Map props = new HashMap<>(); props.put("bootstrap.servers", "localhost:9092"); @@ -57,48 +80,73 @@ private AntifraudResponse deserializeAntifraudResponse(String json) { } } - public Mono createTransaction(int amount) { - Transaction transaction = new Transaction(amount, "PENDING"); - Transaction savedTransaction = transactionRepository.save(transaction); + @Transactional + public Mono createTransaction(TransactionRequest request) { + TransactionStatus transactionStatus = transactionStatusRepository.findById(1L).get(); + TransactionType transactionType = transactionTypeRepository.findById(request.getTranferTypeId()).get(); + + Transactions savedTransactions = transactionRepository.save(Transactions + .builder() + .accountExternalIdCredit(request.getAccountExternalIdCredit()) + .accountExternalIdDebit(request.getAccountExternalIdDebit()) + .transactionStatus(transactionStatus) + .transactionType(transactionType) + .amount(request.getAmount()) + .build()); + + TransactionMessage transactionMessage = TransactionMessage + .builder() + .transactionExternalId(savedTransactions.getId()) + .transactionStatus(TransactionStatusDto.builder().name(savedTransactions.getTransactionStatus().getName()).build()) + .transactionType(TransactionTypeDto.builder().name(savedTransactions.getTransactionType().getName()).build()) + .amount(savedTransactions.getAmount()) + .createdAt(LocalDateTime.now()) + .build(); - return publishTransactionEvent(savedTransaction).thenReturn(savedTransaction); + return publishTransactionEvent(transactionMessage) + .thenReturn(savedTransactions); } private Mono handleAntifraudResponse(AntifraudResponse response) { return Mono.just(response) .flatMap(r -> { return Mono.justOrEmpty(transactionRepository.findById(r.getTransactionId())) - .flatMap(transaction -> { + .flatMap(transactions -> { if (r.isFraudulent()) { - transaction.setStatus("REJECTED"); + TransactionStatus transactionStatus = transactionStatusRepository.findById(3L).get(); + transactions.setTransactionStatus(transactionStatus); } else { - transaction.setStatus("APPROVED"); + TransactionStatus transactionStatus = transactionStatusRepository.findById(2L).get(); + transactions.setTransactionStatus(transactionStatus); } - transactionRepository.save(transaction); + transactionRepository.save(transactions); return Mono.empty(); }); }); } - public Mono getTransaction(Long id) { + public Mono getTransaction(UUID id) { return Mono.justOrEmpty(transactionRepository.findById(id)); } - public Flux getAllTransactions() { + public Flux getAllTransactions() { return Flux.fromIterable(transactionRepository.findAll()); } - private Mono publishTransactionEvent(Transaction transaction) { - return Mono.just(transaction) + private Mono publishTransactionEvent(TransactionMessage transactionMessage) { + return Mono.just(transactionMessage) .map(t -> { try { + log.info("1Publicación exitosa de transactionMessage: {} ", objectMapper.writeValueAsString(t)); return objectMapper.writeValueAsString(t); } catch (Exception e) { throw new RuntimeException("Failed to serialize transaction", e); } }) .flatMap(jsonTransaction -> { - SenderRecord senderRecord = SenderRecord.create(topicName, null, null, transaction.getId().toString(), jsonTransaction, null); + log.info("2Publicación exitosa de transactionMessage: {} ", jsonTransaction); + SenderRecord senderRecord = SenderRecord.create(topicName, null, + null, transactionMessage.getTransactionExternalId().toString(), jsonTransaction, null); return kafkaSender.send(Mono.just(senderRecord)).then(); }); } diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/graphql/TransactionGraphQLResolver.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/graphql/TransactionGraphQLResolver.java index ac47461..548d581 100644 --- a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/graphql/TransactionGraphQLResolver.java +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/graphql/TransactionGraphQLResolver.java @@ -1,14 +1,15 @@ package com.yape.reto.tecnico.kafka.service.graphql; -import com.yape.reto.tecnico.kafka.model.Transaction; +import com.yape.reto.tecnico.kafka.entity.Transactions; +import com.yape.reto.tecnico.kafka.model.TransactionRequest; import com.yape.reto.tecnico.kafka.service.TransactionService; import org.springframework.stereotype.Component; import graphql.kickstart.tools.GraphQLQueryResolver; import graphql.kickstart.tools.GraphQLMutationResolver; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +import java.util.UUID; @Component public class TransactionGraphQLResolver implements GraphQLQueryResolver, GraphQLMutationResolver { @@ -19,15 +20,15 @@ public TransactionGraphQLResolver(TransactionService transactionService) { this.transactionService = transactionService; } - public Mono getTransactionById(Long id) { + public Mono getTransactionById(UUID id) { return transactionService.getTransaction(id); } - public Mono> allTransactions() { + public Mono> allTransactions() { return transactionService.getAllTransactions().collectList(); } - public Mono createTransaction(int amount) { - return transactionService.createTransaction(amount); + public Mono createTransaction(TransactionRequest request) { + return transactionService.createTransaction(request); } } diff --git a/app-microservice-kafka/src/main/resources/application.yml b/app-microservice-kafka/src/main/resources/application.yml index a4e652f..8f8cf2e 100644 --- a/app-microservice-kafka/src/main/resources/application.yml +++ b/app-microservice-kafka/src/main/resources/application.yml @@ -5,10 +5,18 @@ spring: username: sa password: password jpa: + hibernate: + ddl-auto: create-drop database-platform: org.hibernate.dialect.H2Dialect + defer-datasource-initialization: true h2: console: enabled: true + path: /h2-console + sql: + init: + mode: always + data-locations: classpath:query.sql server: port: 8080 diff --git a/app-microservice-kafka/src/main/resources/graphql/schema.graphqls b/app-microservice-kafka/src/main/resources/graphql/schema.graphqls index b9be24a..34dfe9e 100644 --- a/app-microservice-kafka/src/main/resources/graphql/schema.graphqls +++ b/app-microservice-kafka/src/main/resources/graphql/schema.graphqls @@ -1,7 +1,27 @@ +input TransactionRequest { + accountExternalIdDebit: String! + accountExternalIdCredit: String! + tranferTypeId: Int! + amount: Float! +} + type Transaction { id: ID! - amount: Int! - status: String + amount: Float! + accountExternalIdDebit: String + accountExternalIdCredit: String + transactionType: TransactionType + transactionStatus: TransactionStatus +} + +type TransactionType { + id: ID! + name: String +} + +type TransactionStatus { + id: ID! + name: String } type Query { @@ -10,5 +30,5 @@ type Query { } type Mutation { - createTransaction(amount: Int!): Transaction + createTransaction(input: TransactionRequest!): Transaction } diff --git a/app-microservice-kafka/src/main/resources/query.sql b/app-microservice-kafka/src/main/resources/query.sql new file mode 100644 index 0000000..2250842 --- /dev/null +++ b/app-microservice-kafka/src/main/resources/query.sql @@ -0,0 +1,6 @@ +INSERT INTO TRANSACTION_TYPE (id, name) VALUES (1, 'INTERBANCARIA'); +INSERT INTO TRANSACTION_TYPE (id, name) VALUES (2, 'DIRECTA'); + +INSERT INTO transaction_status (id, name) VALUES (1, 'PENDING'); +INSERT INTO transaction_status (id, name) VALUES (2, 'APPROVED'); +INSERT INTO transaction_status (id, name) VALUES (3, 'REJECTED'); From 5affadeadae1abf3e1bf17463b01988032a4cd7b Mon Sep 17 00:00:00 2001 From: Wilmer Palomino Date: Wed, 21 Aug 2024 22:36:51 -0500 Subject: [PATCH 3/4] feat: configure docker compose mwith microservices --- README.md | 10 +++++++++- app-antifraud-microservice-kafka/Dockerfile | 13 +++++++++++++ app-antifraud-microservice-kafka/pom.xml | 16 ++++++++++++++++ .../antifraud/service/AntifraudService.java | 2 +- .../src/main/resources/application.yml | 6 +++++- app-microservice-kafka/Dockerfile | 13 +++++++++++++ app-microservice-kafka/pom.xml | 17 ++++++++++++++++- .../kafka/service/TransactionService.java | 2 +- .../src/main/resources/application.yml | 2 ++ docker-compose.yml | 18 ++++++++++++++++++ 10 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 app-antifraud-microservice-kafka/Dockerfile create mode 100644 app-microservice-kafka/Dockerfile diff --git a/README.md b/README.md index 296ffbb..4ce8cf8 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +# Solution + +To solve the technical challenge, apply everything requested, first create a microservice that subscribes to Kafka topics and with this can create transactions in the DB and in the respective Topic and the second microservice is responsible for subscribing to the Kafka topic. creation to obtain the amount and be able to validate whether it is fraudulent or not according to the condition provided, in case the fraud microservice is restarted it will resume all the requests that were pending to analyze whether or not it was a fraud, the technological stack en Reactive programming with Java 17 (webflux), as well as GraphQL, H2 database with JPA, were used for the requests. + + # Yape Code Challenge :rocket: Our code challenge will let you marvel us with your Jedi coding skills :smile:. @@ -9,6 +14,7 @@ Don't forget that the proper way to submit your work is to fork the repo and cre - [Tech Stack](#tech-stack) - [Optional](#optional) - [Send us your challenge](#send-us-your-challenge) +- [Solution](#solution) # Problem @@ -81,4 +87,6 @@ You can use Graphql; When you finish your challenge, after forking a repository, you **must** open a pull request to our repository. There are no limitations to the implementation, you can follow the programming paradigm, modularization, and style that you feel is the most appropriate solution. -If you have any questions, please let us know. \ No newline at end of file +If you have any questions, please let us know. + + diff --git a/app-antifraud-microservice-kafka/Dockerfile b/app-antifraud-microservice-kafka/Dockerfile new file mode 100644 index 0000000..3cd81a3 --- /dev/null +++ b/app-antifraud-microservice-kafka/Dockerfile @@ -0,0 +1,13 @@ +# Etapa 1: Compilación de la aplicación +FROM maven:3.8.8-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn clean package -DskipTests + +# Etapa 2: Construcción de la imagen +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY --from=build /app/target/app-antifraud-microservice-kafka-1.0-SNAPSHOT.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "/app/app.jar"] \ No newline at end of file diff --git a/app-antifraud-microservice-kafka/pom.xml b/app-antifraud-microservice-kafka/pom.xml index b86017c..202cb70 100644 --- a/app-antifraud-microservice-kafka/pom.xml +++ b/app-antifraud-microservice-kafka/pom.xml @@ -95,4 +95,20 @@ + + + + org.springframework.boot + spring-boot-maven-plugin + 3.1.0 + + + + repackage + + + + + + \ No newline at end of file diff --git a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java index 8d96b42..fbadf5a 100644 --- a/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java +++ b/app-antifraud-microservice-kafka/src/main/java/com/yape/reto/tecnico/antifraud/service/AntifraudService.java @@ -27,7 +27,7 @@ public class AntifraudService { public AntifraudService() { objectMapper.registerModule(new JavaTimeModule()); Map props = new HashMap<>(); - props.put("bootstrap.servers", "localhost:9092"); + props.put("bootstrap.servers", "kafka:29092"); props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer"); props.put("auto.offset.reset", "earliest"); diff --git a/app-antifraud-microservice-kafka/src/main/resources/application.yml b/app-antifraud-microservice-kafka/src/main/resources/application.yml index 58de10c..12f5435 100644 --- a/app-antifraud-microservice-kafka/src/main/resources/application.yml +++ b/app-antifraud-microservice-kafka/src/main/resources/application.yml @@ -1,2 +1,6 @@ server: - port: 8090 \ No newline at end of file + port: 8090 + +spring: + kafka: + bootstrap-servers: kafka:9092 \ No newline at end of file diff --git a/app-microservice-kafka/Dockerfile b/app-microservice-kafka/Dockerfile new file mode 100644 index 0000000..3250663 --- /dev/null +++ b/app-microservice-kafka/Dockerfile @@ -0,0 +1,13 @@ +# Etapa 1: Compilación de la aplicación +FROM maven:3.8.8-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn clean package -DskipTests + +# Etapa 2: Construcción de la imagen +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY --from=build /app/target/app-microservice-kafka-1.0-SNAPSHOT.jar app.jar +EXPOSE 8081 +ENTRYPOINT ["java", "-jar", "/app/app.jar"] \ No newline at end of file diff --git a/app-microservice-kafka/pom.xml b/app-microservice-kafka/pom.xml index d612ca4..ba7e0dc 100644 --- a/app-microservice-kafka/pom.xml +++ b/app-microservice-kafka/pom.xml @@ -94,6 +94,21 @@ 17.0 - + + + + org.springframework.boot + spring-boot-maven-plugin + 3.1.0 + + + + repackage + + + + + + \ No newline at end of file diff --git a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java index 1230f67..86b8cd2 100644 --- a/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java +++ b/app-microservice-kafka/src/main/java/com/yape/reto/tecnico/kafka/service/TransactionService.java @@ -52,7 +52,7 @@ public TransactionService(TransactionRepository transactionRepository, Transacti objectMapper.registerModule(new JavaTimeModule()); Map props = new HashMap<>(); - props.put("bootstrap.servers", "localhost:9092"); + props.put("bootstrap.servers", "kafka:29092"); props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer"); props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer"); SenderOptions senderOptions = SenderOptions.create(props); diff --git a/app-microservice-kafka/src/main/resources/application.yml b/app-microservice-kafka/src/main/resources/application.yml index 8f8cf2e..9f239a2 100644 --- a/app-microservice-kafka/src/main/resources/application.yml +++ b/app-microservice-kafka/src/main/resources/application.yml @@ -17,6 +17,8 @@ spring: init: mode: always data-locations: classpath:query.sql + kafka: + bootstrap-servers: kafka:9092 server: port: 8080 diff --git a/docker-compose.yml b/docker-compose.yml index 6ac96e3..11a325d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,4 +31,22 @@ services: ZK_HOSTS: zookeeper:2181 depends_on: - zookeeper + - kafka + app-microservice: + build: + context: ./app-microservice-kafka + dockerfile: Dockerfile + ports: + - "8080:8080" + depends_on: + - postgres + - kafka + + app-antifraud: + build: + context: ./app-antifraud-microservice-kafka + dockerfile: Dockerfile + ports: + - "8081:8081" + depends_on: - kafka \ No newline at end of file From 9e50ce3eaa75f817fecb17fe0ec61126e6445fb3 Mon Sep 17 00:00:00 2001 From: Wilmer Palomino Date: Wed, 21 Aug 2024 22:44:16 -0500 Subject: [PATCH 4/4] feat: add postman collection and readme --- README.md | 8 +- .../Yape collection.postman_collection.json | 106 ++++++++++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 postman/Yape collection.postman_collection.json diff --git a/README.md b/README.md index 4ce8cf8..4205021 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,13 @@ To solve the technical challenge, apply everything requested, first create a microservice that subscribes to Kafka topics and with this can create transactions in the DB and in the respective Topic and the second microservice is responsible for subscribing to the Kafka topic. creation to obtain the amount and be able to validate whether it is fraudulent or not according to the condition provided, in case the fraud microservice is restarted it will resume all the requests that were pending to analyze whether or not it was a fraud, the technological stack en Reactive programming with Java 17 (webflux), as well as GraphQL, H2 database with JPA, were used for the requests. +# Deploy Local + +To deploy only run "docker compose up --build -d" with this command all microservices and components will be start and ready for use. + +# Test + +You can test the endpoints of microservices with the postman collection, it's located in directory "postman" # Yape Code Challenge :rocket: @@ -14,7 +21,6 @@ Don't forget that the proper way to submit your work is to fork the repo and cre - [Tech Stack](#tech-stack) - [Optional](#optional) - [Send us your challenge](#send-us-your-challenge) -- [Solution](#solution) # Problem diff --git a/postman/Yape collection.postman_collection.json b/postman/Yape collection.postman_collection.json new file mode 100644 index 0000000..f111a28 --- /dev/null +++ b/postman/Yape collection.postman_collection.json @@ -0,0 +1,106 @@ +{ + "info": { + "_postman_id": "c71a9ece-4559-4ef3-b777-7d4ee0a4c42f", + "name": "Yape collection", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "5720344" + }, + "item": [ + { + "name": "Get Transaction By Id", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "graphql", + "graphql": { + "query": "query {\r\n transactionById(id: \"1\") {\r\n id\r\n amount\r\n status\r\n }\r\n}", + "variables": "" + } + }, + "url": { + "raw": "http://localhost:8080/graphql", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "graphql" + ] + } + }, + "response": [] + }, + { + "name": "Get Transaction ALL", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "graphql", + "graphql": { + "query": "query {\r\n allTransactions {\r\n id\r\n amount\r\n accountExternalIdDebit\r\n accountExternalIdCredit\r\n transactionType {\r\n name\r\n }\r\n transactionStatus {\r\n name\r\n }\r\n }\r\n}", + "variables": "" + } + }, + "url": { + "raw": "http://localhost:8080/graphql", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "graphql" + ] + } + }, + "response": [] + }, + { + "name": "create Transaction", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json", + "type": "text" + } + ], + "body": { + "mode": "graphql", + "graphql": { + "query": "mutation {\r\n createTransaction(input: {\r\n accountExternalIdDebit: \"123456789\",\r\n accountExternalIdCredit: \"987654321\",\r\n tranferTypeId: 1,\r\n amount: 2222.50\r\n }) {\r\n id\r\n amount\r\n accountExternalIdDebit\r\n accountExternalIdCredit\r\n }\r\n}\r\n", + "variables": "" + } + }, + "url": { + "raw": "http://localhost:8080/graphql", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "graphql" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file