From ce48ed4de9d31ae7f8cba069c07eadc4e67b7584 Mon Sep 17 00:00:00 2001 From: Karl Date: Tue, 11 Jun 2024 23:26:25 -0500 Subject: [PATCH 1/2] First commit --- .DS_Store | Bin 0 -> 6148 bytes .gitignore | 68 +++++ README.md | 114 ++++---- antifraud-yape/.gitignore | 37 +++ antifraud-yape/Dockerfile | 9 + antifraud-yape/build.gradle | 39 +++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + antifraud-yape/gradlew | 249 ++++++++++++++++++ antifraud-yape/gradlew.bat | 92 +++++++ antifraud-yape/settings.gradle | 1 + .../antifraud/AntifraudYapeApplication.java | 13 + .../entities/enums/TransactionStatus.java | 8 + .../entities/enums/TransactionType.java | 6 + .../entities/enums/TransferType.java | 6 + .../entities/models/Transaction.java | 25 ++ .../in/AntifraudKafkaConsumer.java | 24 ++ .../models/AntifraudMessageModel.java | 23 ++ .../models/TransactionMessageModel.java | 23 ++ .../out/TransactionKafkaGateway.java | 29 ++ .../usecases/AntifraudValidateUseCase.java | 40 +++ .../in/AntifraudValidateInputBoundary.java | 8 + .../models/AntifraudValidateModel.java | 24 ++ .../usecases/out/TransactionGateway.java | 7 + .../src/main/resources/application.yml | 35 +++ .../AntifraudYapeApplicationTests.java | 11 + .../AntifraudValidateUseCaseTests.java | 62 +++++ docker-compose.yml | 46 +++- transaction-yape/.gitignore | 37 +++ transaction-yape/Dockerfile | 9 + transaction-yape/build.gradle | 46 ++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + transaction-yape/gradlew | 249 ++++++++++++++++++ transaction-yape/gradlew.bat | 92 +++++++ transaction-yape/settings.gradle | 1 + .../TransactionYapeApplication.java | 15 ++ .../entities/enums/TransactionStatus.java | 7 + .../entities/enums/TransactionType.java | 6 + .../entities/enums/TransferType.java | 15 ++ .../entities/models/Transaction.java | 31 +++ .../adapter/ExceptionGraphQLAdapter.java | 35 +++ .../infrastructure/config/RedisConfig.java | 54 ++++ .../infrastructure/in/TransactionGraphQL.java | 43 +++ .../in/TransactionKafkaConsumer.java | 23 ++ .../in/TransactionRestController.java | 63 +++++ .../models/AntifraudMessageModel.java | 23 ++ .../models/CreateTransactionRequestModel.java | 42 +++ .../models/GetTransactionRequestModel.java | 27 ++ .../models/GetTransactionResponseModel.java | 40 +++ .../models/TransactionMessageModel.java | 25 ++ .../models/UpdateTransactionRequestModel.java | 15 ++ .../persistance/TransactionRepository.java | 11 + .../models/persistance/TransactionSchema.java | 80 ++++++ .../out/AntifraudKafkaGateway.java | 29 ++ .../out/TransactionJpaGateway.java | 48 ++++ .../usecases/CreateTransactionUseCase.java | 41 +++ .../usecases/GetTransactionUseCase.java | 30 +++ .../usecases/UpdateTransactionUseCase.java | 37 +++ .../in/CreateTransactionInputBoundary.java | 8 + .../in/GetTransactionInputBoundary.java | 8 + .../in/UpdateTransactionInputBoundary.java | 8 + .../models/CreateTransactionModel.java | 26 ++ .../usecases/models/GetTransactionModel.java | 19 ++ .../models/UpdateTransactionModel.java | 22 ++ .../TransactionNotFoundException.java | 7 + .../usecases/out/AntifraudGateway.java | 7 + .../out/TransactionDataAccessGateway.java | 9 + .../src/main/resources/application.yml | 69 +++++ .../resources/graphql/transaction.graphqls | 34 +++ .../TransactionYapeApplicationTests.java | 11 + .../CreateTransactionUseCaseTests.java | 55 ++++ .../usecases/GetTransactionUseCaseTests.java | 59 +++++ .../UpdateTransactionUseCaseTests.java | 74 ++++++ 74 files changed, 2541 insertions(+), 62 deletions(-) create mode 100644 .DS_Store create mode 100644 .gitignore create mode 100644 antifraud-yape/.gitignore create mode 100644 antifraud-yape/Dockerfile create mode 100644 antifraud-yape/build.gradle create mode 100644 antifraud-yape/gradle/wrapper/gradle-wrapper.jar create mode 100644 antifraud-yape/gradle/wrapper/gradle-wrapper.properties create mode 100755 antifraud-yape/gradlew create mode 100644 antifraud-yape/gradlew.bat create mode 100644 antifraud-yape/settings.gradle create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/AntifraudYapeApplication.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransactionStatus.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransactionType.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransferType.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/entities/models/Transaction.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/in/AntifraudKafkaConsumer.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/models/AntifraudMessageModel.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/models/TransactionMessageModel.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/out/TransactionKafkaGateway.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/usecases/AntifraudValidateUseCase.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/usecases/in/AntifraudValidateInputBoundary.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/usecases/models/AntifraudValidateModel.java create mode 100644 antifraud-yape/src/main/java/com/yape/antifraud/usecases/out/TransactionGateway.java create mode 100644 antifraud-yape/src/main/resources/application.yml create mode 100644 antifraud-yape/src/test/java/com/yape/antifraud/AntifraudYapeApplicationTests.java create mode 100644 antifraud-yape/src/test/java/com/yape/antifraud/usecases/AntifraudValidateUseCaseTests.java create mode 100644 transaction-yape/.gitignore create mode 100644 transaction-yape/Dockerfile create mode 100644 transaction-yape/build.gradle create mode 100644 transaction-yape/gradle/wrapper/gradle-wrapper.jar create mode 100644 transaction-yape/gradle/wrapper/gradle-wrapper.properties create mode 100755 transaction-yape/gradlew create mode 100644 transaction-yape/gradlew.bat create mode 100644 transaction-yape/settings.gradle create mode 100644 transaction-yape/src/main/java/com/yape/transaction/TransactionYapeApplication.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransactionStatus.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransactionType.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransferType.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/entities/models/Transaction.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/adapter/ExceptionGraphQLAdapter.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/config/RedisConfig.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionGraphQL.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionKafkaConsumer.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionRestController.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/AntifraudMessageModel.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/CreateTransactionRequestModel.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/GetTransactionRequestModel.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/GetTransactionResponseModel.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/TransactionMessageModel.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/UpdateTransactionRequestModel.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/persistance/TransactionRepository.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/persistance/TransactionSchema.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/out/AntifraudKafkaGateway.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/infrastructure/out/TransactionJpaGateway.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/CreateTransactionUseCase.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/GetTransactionUseCase.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/UpdateTransactionUseCase.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/in/CreateTransactionInputBoundary.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/in/GetTransactionInputBoundary.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/in/UpdateTransactionInputBoundary.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/models/CreateTransactionModel.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/models/GetTransactionModel.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/models/UpdateTransactionModel.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/models/exception/TransactionNotFoundException.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/out/AntifraudGateway.java create mode 100644 transaction-yape/src/main/java/com/yape/transaction/usecases/out/TransactionDataAccessGateway.java create mode 100644 transaction-yape/src/main/resources/application.yml create mode 100644 transaction-yape/src/main/resources/graphql/transaction.graphqls create mode 100644 transaction-yape/src/test/java/com/yape/transaction/TransactionYapeApplicationTests.java create mode 100644 transaction-yape/src/test/java/com/yape/transaction/usecases/CreateTransactionUseCaseTests.java create mode 100644 transaction-yape/src/test/java/com/yape/transaction/usecases/GetTransactionUseCaseTests.java create mode 100644 transaction-yape/src/test/java/com/yape/transaction/usecases/UpdateTransactionUseCaseTests.java diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c445652771a603f23afd8ccdb5ac3928b6017935 GIT binary patch literal 6148 zcmeH~-D=w~6vvO6taTbze6c|;1-nhC^U@7WRx*3eT2ogF{#Y0x;Op&$ z_9T0rog*n@S=JUx86oJv(O)0u$mj=>EC67mGVTJn0N|jL)(^4xhtW9shV7Z2Ix16h ze1;4{C?STS(|r<-DR6Ep7*>qAM{U`%ZBsx+3}0lldJSv$Zr}d z!!tj^!ssMq;Z2Tf$`E4cffd@BL;n^iJ-< zTVD)`^9PQ#&YZ`ECEa40SVCgz|l#4qm@>5bYP*L0I2jEp$zNvmY^JN z)Hhmb#St{7Q&Dv)Gfxbr)3KkMINxZcRi^_pj}Kf>NM!X=rzbz^aK*K+ia=wuXEX;mp$nCn -
  • pending
  • -
  • approved
  • -
  • rejected
  • - +### Arquitectura del Proyecto -Every transaction with a value greater than 1000 should be rejected. +Para la solucion a este problema se decidió implementar Clean Architecture el cual se encuentra definido en el libro Clean Architecture por Robert C. Martin, este tipo de arquitectura trata de englobar las arquitecturas previamente existentes tales como Hexagonal, Onion, etc. Y nos brinda ciertas ventajas tales como: facilidad para realizar tests, independencia de frameworks, independencia de la base de datos, independencia de cualquier agente externo y independencia de UI. -```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)] -``` +### Estructura del Proyecto +El proyecto ha sido dividido en 3 capas: + +- Entities: en esta capa de almacenan todas las entidades de negocio (estos pueden ser simples estructuras de datos con metodos), son los componentes de mas alto nivel, debido a esto, son los que muy probablemente no cambien debido a cambios externos. +- Uses cases: en esta capa se encuentran las reglas de negocio de la aplicacion, esto significa que los componentes en esta capa se encargan de administrar las entidades de negocio y el flujo de los datos. +- Infrastructure: en esta capa tenemos todos los componentes de mas bajo nivel tales como componentes de base de datos, framework GraphQL, framework RestController, por los que estos componentes estan destinados como conexion con otras aplicaciones externos. + +### Componentes de la solucion + +- Antifraud: este es un microservicio construido usando Spring Boot el cual tiene como objetivo validar el monto de las transacciones creadas en el componente Transaction, este componente es un Consumer Kafka y recibe los mensajes debido a que esta suscrito al topic "antifraud". Despues de realizar la validacion, mandara un mensaje al topic "transaction" para actualizar el estado de la transaccion "APPROVED" or "REJECTED", esto lo logra debido a que tambien es un Producer kafka. + +- Transaction: este es un microservicio construido con Spring Boot el cual tiene como objetivo realizar distintas operaciones a las transacciones, tiene metodos para crear, modificar estado y obtener una transaccion. Tiene una conexion a la base de datos Postgres para realizar dichas operaciones. Este microservicio es un Producer Kafka porque durante el proceso de crear una transaccion, este componente envia un mensaje al topic "antifraud" para que el microservicio Antifraud valide el monto de la transaccion. Ademas este microservicio se comporta como un Consumer para que pueda recibir las peticiones de modificacion de estado que envia el componente "antifraud". + +- Kafka: este componente es utilizado para la comunicacion entre el microservicio de Antifraud y Transaction. Ademas, utiliza 2 topics: "antifraud" y "transaction", el primero sirve para validar el monto de las transacciones, el consumer es el microservicio Antifraud, el segundo es utilizado para actualizar el estado de la transaccion, el consumer de este topic es el microservicio de "Transaction". + +- Postgres: es una base de datos relacional que servira para persistir las transacciones, contendra una sola tabla Transaction con todas las columnas necesarias para guardar la informacion de la transaccion. El microservicio de Transaction se coneectara a esta base de datos utilizando un pool de conexiones que sera administrada por el framework HikariCP. + +- Redis: Componente que funcionara como cache para la operacion de consultar transacciones. -# Tech Stack +### Endpoints -
      -
    1. Java. You can use any framework you want
    2. -
    3. Any database
    4. -
    5. Kafka
    6. -
    +Se cuentan con los siguientes recursos en GraphQL: -We do provide a `Dockerfile` to help you get started with a dev environment. +- Mutation: createTransaction: Crear transaction y validarlo +- Query: getTransaction: Obtener una transaction por codigo -You must have two resources: +## Levantar proyecto localmente -1. Resource to create a transaction that must containt: +Clone the project -```json -{ - "accountExternalIdDebit": "Guid", - "accountExternalIdCredit": "Guid", - "tranferTypeId": 1, - "value": 120 -} +```bash + git clone ``` -2. Resource to retrieve a transaction - -```json -{ - "transactionExternalId": "Guid", - "transactionType": { - "name": "" - }, - "transactionStatus": { - "name": "" - }, - "value": 120, - "createdAt": "Date" -} +Go to the project directory + +```bash + cd my-project +``` + +Start all the components + +```bash + docker-compose up ``` -## 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? +## Correr tests + +To run tests, run the following command + +```bash + ./gradlew clean test --info +``` -You can use Graphql; +## Requisitos -# Send us your challenge +Tener instalado docker. -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. +## Autor -If you have any questions, please let us know. \ No newline at end of file +- Karl Renzo Alcala Paucar diff --git a/antifraud-yape/.gitignore b/antifraud-yape/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/antifraud-yape/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/antifraud-yape/Dockerfile b/antifraud-yape/Dockerfile new file mode 100644 index 0000000..8c550cb --- /dev/null +++ b/antifraud-yape/Dockerfile @@ -0,0 +1,9 @@ +FROM gradle:jdk17 AS BUILD +WORKDIR /usr/app/ +COPY antifraud-yape . +RUN gradle build + +FROM eclipse-temurin:17 +WORKDIR /usr/app/ +COPY --from=BUILD /usr/app/ . +ENTRYPOINT ["java","-jar","build/libs/antifraud-yape-0.0.1-SNAPSHOT.jar"] \ No newline at end of file diff --git a/antifraud-yape/build.gradle b/antifraud-yape/build.gradle new file mode 100644 index 0000000..0784708 --- /dev/null +++ b/antifraud-yape/build.gradle @@ -0,0 +1,39 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.0' + id 'io.spring.dependency-management' version '1.1.5' +} + +group = 'com.yape' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.kafka:spring-kafka' + compileOnly 'org.projectlombok:lombok' + developmentOnly 'org.springframework.boot:spring-boot-devtools' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/antifraud-yape/gradle/wrapper/gradle-wrapper.jar b/antifraud-yape/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
    NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/antifraud-yape/gradlew.bat b/antifraud-yape/gradlew.bat new file mode 100644 index 0000000..7101f8e --- /dev/null +++ b/antifraud-yape/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/antifraud-yape/settings.gradle b/antifraud-yape/settings.gradle new file mode 100644 index 0000000..b6a797c --- /dev/null +++ b/antifraud-yape/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'antifraud-yape' diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/AntifraudYapeApplication.java b/antifraud-yape/src/main/java/com/yape/antifraud/AntifraudYapeApplication.java new file mode 100644 index 0000000..58aa578 --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/AntifraudYapeApplication.java @@ -0,0 +1,13 @@ +package com.yape.antifraud; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AntifraudYapeApplication { + + public static void main(String[] args) { + SpringApplication.run(AntifraudYapeApplication.class, args); + } + +} diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransactionStatus.java b/antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransactionStatus.java new file mode 100644 index 0000000..789335a --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransactionStatus.java @@ -0,0 +1,8 @@ +package com.yape.antifraud.entities.enums; + +public enum TransactionStatus { + PENDING, + APPROVED, + REJECTED; + +} diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransactionType.java b/antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransactionType.java new file mode 100644 index 0000000..47c29b5 --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransactionType.java @@ -0,0 +1,6 @@ +package com.yape.antifraud.entities.enums; + +public enum TransactionType { + WITHDRAWAL, + DEPOSIT; +} diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransferType.java b/antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransferType.java new file mode 100644 index 0000000..f7ad805 --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/entities/enums/TransferType.java @@ -0,0 +1,6 @@ +package com.yape.antifraud.entities.enums; + +public enum TransferType { + NATIONAL, + INTERNATIONAL; +} diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/entities/models/Transaction.java b/antifraud-yape/src/main/java/com/yape/antifraud/entities/models/Transaction.java new file mode 100644 index 0000000..241ceeb --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/entities/models/Transaction.java @@ -0,0 +1,25 @@ +package com.yape.antifraud.entities.models; + +import com.yape.antifraud.entities.enums.TransactionStatus; +import com.yape.antifraud.entities.enums.TransactionType; +import com.yape.antifraud.entities.enums.TransferType; +import lombok.Builder; +import lombok.Data; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@Builder +public class Transaction { + private Long id; + private UUID transactionExternalId; + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private Double value; + private TransferType transferType; + private TransactionType transactionType; + private TransactionStatus transactionStatus; + private LocalDateTime createAt; + private LocalDateTime updateAt; +} \ No newline at end of file diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/in/AntifraudKafkaConsumer.java b/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/in/AntifraudKafkaConsumer.java new file mode 100644 index 0000000..373cc8e --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/in/AntifraudKafkaConsumer.java @@ -0,0 +1,24 @@ +package com.yape.antifraud.infrastructure.in; + +import com.yape.antifraud.infrastructure.models.AntifraudMessageModel; +import com.yape.antifraud.usecases.in.AntifraudValidateInputBoundary; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class AntifraudKafkaConsumer { + + private final AntifraudValidateInputBoundary antifraudValidateInputBoundary; + + public AntifraudKafkaConsumer(AntifraudValidateInputBoundary antifraudValidateInputBoundary) { + this.antifraudValidateInputBoundary = antifraudValidateInputBoundary; + } + + @KafkaListener(topics = "${spring.kafka.topic.antifraud-validate}", groupId = "${spring.kafka.consumer.group-id}") + public void antifraudValidate(AntifraudMessageModel antifraudMessageModel) { + log.info("AntifraudKafkaConsumer antifraudValidate antifraudMessageModel {}", antifraudMessageModel); + antifraudValidateInputBoundary.antifraudValidate(antifraudMessageModel.getUseCaseModel()); + } +} \ No newline at end of file diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/models/AntifraudMessageModel.java b/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/models/AntifraudMessageModel.java new file mode 100644 index 0000000..fb53daa --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/models/AntifraudMessageModel.java @@ -0,0 +1,23 @@ +package com.yape.antifraud.infrastructure.models; + +import com.yape.antifraud.usecases.models.AntifraudValidateModel; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Data +@NoArgsConstructor +public class AntifraudMessageModel { + private String transactionExternalId; + private Double value; + private String status; + + public AntifraudValidateModel getUseCaseModel() { + return AntifraudValidateModel.builder() + .transactionExternalId(UUID.fromString(transactionExternalId)) + .value(value) + .status(status) + .build(); + } +} diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/models/TransactionMessageModel.java b/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/models/TransactionMessageModel.java new file mode 100644 index 0000000..43778c3 --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/models/TransactionMessageModel.java @@ -0,0 +1,23 @@ +package com.yape.antifraud.infrastructure.models; + +import com.yape.antifraud.entities.models.Transaction; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TransactionMessageModel { + private String transactionExternalId; + private String status; + + public static TransactionMessageModel getInstanceFrom(Transaction transaction) { + return TransactionMessageModel.builder() + .transactionExternalId(transaction.getTransactionExternalId().toString()) + .status(transaction.getTransactionStatus().name()) + .build(); + } +} diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/out/TransactionKafkaGateway.java b/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/out/TransactionKafkaGateway.java new file mode 100644 index 0000000..197681e --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/infrastructure/out/TransactionKafkaGateway.java @@ -0,0 +1,29 @@ +package com.yape.antifraud.infrastructure.out; + +import com.yape.antifraud.usecases.out.TransactionGateway; +import com.yape.antifraud.entities.models.Transaction; +import com.yape.antifraud.infrastructure.models.TransactionMessageModel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class TransactionKafkaGateway implements TransactionGateway { + + @Value("${spring.kafka.topic.transaction-update}") + private String kafkaTopicTransaction; + + private final KafkaTemplate kafkaTemplate; + + public TransactionKafkaGateway(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Override + public void updateStatus(Transaction transaction) { + log.info("TransactionKafkaGateway updateStatus transaction {}", transaction); + kafkaTemplate.send(kafkaTopicTransaction, TransactionMessageModel.getInstanceFrom(transaction)); + } +} diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/usecases/AntifraudValidateUseCase.java b/antifraud-yape/src/main/java/com/yape/antifraud/usecases/AntifraudValidateUseCase.java new file mode 100644 index 0000000..f18eb8b --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/usecases/AntifraudValidateUseCase.java @@ -0,0 +1,40 @@ +package com.yape.antifraud.usecases; + +import com.yape.antifraud.usecases.in.AntifraudValidateInputBoundary; +import com.yape.antifraud.usecases.models.AntifraudValidateModel; +import com.yape.antifraud.usecases.out.TransactionGateway; +import com.yape.antifraud.entities.enums.TransactionStatus; +import com.yape.antifraud.entities.models.Transaction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class AntifraudValidateUseCase implements AntifraudValidateInputBoundary { + + private final TransactionGateway transactionGateway; + + private final Double limitAmount; + + public AntifraudValidateUseCase(TransactionGateway transactionGateway, + @Value("${antifraud.limit-amount}") Double limitAmount) { + this.transactionGateway = transactionGateway; + this.limitAmount = limitAmount; + } + + @Override + public Transaction antifraudValidate(AntifraudValidateModel antifraudValidateModel) { + log.info("AntifraudValidateUseCase antifraudValidate antifraudValidateModel {}", antifraudValidateModel); + Transaction transaction = antifraudValidateModel.convertEntity(); + + if (transaction.getValue() >= limitAmount) { + transaction.setTransactionStatus(TransactionStatus.REJECTED); + } else { + transaction.setTransactionStatus(TransactionStatus.APPROVED); + } + transactionGateway.updateStatus(transaction); + log.info("AntifraudValidateUseCase antifraudValidate transaction {}", transaction); + return transaction; + } +} diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/usecases/in/AntifraudValidateInputBoundary.java b/antifraud-yape/src/main/java/com/yape/antifraud/usecases/in/AntifraudValidateInputBoundary.java new file mode 100644 index 0000000..73d373a --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/usecases/in/AntifraudValidateInputBoundary.java @@ -0,0 +1,8 @@ +package com.yape.antifraud.usecases.in; + +import com.yape.antifraud.entities.models.Transaction; +import com.yape.antifraud.usecases.models.AntifraudValidateModel; + +public interface AntifraudValidateInputBoundary { + Transaction antifraudValidate(AntifraudValidateModel antifraudValidateModel); +} diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/usecases/models/AntifraudValidateModel.java b/antifraud-yape/src/main/java/com/yape/antifraud/usecases/models/AntifraudValidateModel.java new file mode 100644 index 0000000..bf3d65b --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/usecases/models/AntifraudValidateModel.java @@ -0,0 +1,24 @@ +package com.yape.antifraud.usecases.models; + +import com.yape.antifraud.entities.enums.TransactionStatus; +import com.yape.antifraud.entities.models.Transaction; +import lombok.Builder; +import lombok.Data; + +import java.util.UUID; + +@Data +@Builder +public class AntifraudValidateModel { + private UUID transactionExternalId; + private Double value; + private String status; + + public Transaction convertEntity() { + return Transaction.builder() + .transactionExternalId(this.getTransactionExternalId()) + .value(this.getValue()) + .transactionStatus(TransactionStatus.valueOf(this.getStatus())) + .build(); + } +} diff --git a/antifraud-yape/src/main/java/com/yape/antifraud/usecases/out/TransactionGateway.java b/antifraud-yape/src/main/java/com/yape/antifraud/usecases/out/TransactionGateway.java new file mode 100644 index 0000000..64eb6ea --- /dev/null +++ b/antifraud-yape/src/main/java/com/yape/antifraud/usecases/out/TransactionGateway.java @@ -0,0 +1,7 @@ +package com.yape.antifraud.usecases.out; + +import com.yape.antifraud.entities.models.Transaction; + +public interface TransactionGateway { + void updateStatus(Transaction transaction); +} diff --git a/antifraud-yape/src/main/resources/application.yml b/antifraud-yape/src/main/resources/application.yml new file mode 100644 index 0000000..b36563c --- /dev/null +++ b/antifraud-yape/src/main/resources/application.yml @@ -0,0 +1,35 @@ +server: + port: 8081 + +spring: + application: + name: antifraud-yape + + kafka: + bootstrap-servers: kafka:29092 + #bootstrap-servers: localhost:9092 + topic: + antifraud-validate: antifraud + transaction-update: transaction + producer: + key-serializer: org.springframework.kafka.support.serializer.JsonSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + + consumer: + group-id: antifraud-group + auto-offset-reset: earliest + key-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring: + json: + type: + mapping: 'com.yape.transaction.infrastructure.models.AntifraudMessageModel:com.yape.antifraud.infrastructure.models.AntifraudMessageModel' + trusted: + packages: 'com.yape.transaction.infrastructure.models' + + listener: + missing-topics-fatal: false + +antifraud: + limit-amount: 1000 diff --git a/antifraud-yape/src/test/java/com/yape/antifraud/AntifraudYapeApplicationTests.java b/antifraud-yape/src/test/java/com/yape/antifraud/AntifraudYapeApplicationTests.java new file mode 100644 index 0000000..16ef2ac --- /dev/null +++ b/antifraud-yape/src/test/java/com/yape/antifraud/AntifraudYapeApplicationTests.java @@ -0,0 +1,11 @@ +package com.yape.antifraud; + +import org.junit.jupiter.api.Test; + +class AntifraudYapeApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/antifraud-yape/src/test/java/com/yape/antifraud/usecases/AntifraudValidateUseCaseTests.java b/antifraud-yape/src/test/java/com/yape/antifraud/usecases/AntifraudValidateUseCaseTests.java new file mode 100644 index 0000000..da14fa0 --- /dev/null +++ b/antifraud-yape/src/test/java/com/yape/antifraud/usecases/AntifraudValidateUseCaseTests.java @@ -0,0 +1,62 @@ +package com.yape.antifraud.usecases; + +import com.yape.antifraud.entities.enums.TransactionStatus; +import com.yape.antifraud.entities.models.Transaction; +import com.yape.antifraud.infrastructure.out.TransactionKafkaGateway; +import com.yape.antifraud.usecases.models.AntifraudValidateModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.UUID; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@ExtendWith(MockitoExtension.class) +public class AntifraudValidateUseCaseTests { + + @Mock + private TransactionKafkaGateway transactionKafkaGateway; + + private AntifraudValidateUseCase antifraudValidateUseCase; + + private Double valueLimit = 1000D; + + @BeforeEach + public void beforeAll() { + antifraudValidateUseCase = new AntifraudValidateUseCase(transactionKafkaGateway, valueLimit); + } + + @DisplayName("Antifraud Validate transaction - Successfully (APPROVED)") + @Test + public void updateTransactionSuccessfully_approved(){ + + Transaction transaction = antifraudValidateUseCase.antifraudValidate(AntifraudValidateModel.builder() + .transactionExternalId(UUID.randomUUID()) + .value(999.99) + .status(TransactionStatus.PENDING.name()) + .build()); + + assertThat(transaction).isNotNull(); + assertEquals(transaction.getTransactionStatus().name(), TransactionStatus.APPROVED.name()); + } + + + @DisplayName("Antifraud Validate transaction - Successfully (REJECTED)") + @Test + public void updateTransactionSuccessfully_rejected(){ + + Transaction transaction = antifraudValidateUseCase.antifraudValidate(AntifraudValidateModel.builder() + .transactionExternalId(UUID.randomUUID()) + .value(1000.1) + .status(TransactionStatus.PENDING.name()) + .build()); + + assertThat(transaction).isNotNull(); + assertEquals(transaction.getTransactionStatus().name(), TransactionStatus.REJECTED.name()); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index 6e9a9c5..b0ccaaa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,10 +7,16 @@ services: environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres + networks: + - yape-network + zookeeper: image: confluentinc/cp-zookeeper:5.5.3 environment: ZOOKEEPER_CLIENT_PORT: 2181 + networks: + - yape-network + kafka: image: confluentinc/cp-enterprise-kafka:5.5.3 depends_on: [zookeeper] @@ -22,4 +28,42 @@ services: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_JMX_PORT: 9991 ports: - - 9092:9092 \ No newline at end of file + - 9092:9092 + networks: + - yape-network + + cache: + image: redis:6.2-alpine + restart: always + ports: + - '6379:6379' + command: redis-server --save 20 1 --loglevel warning --requirepass jA29i4X5mt9QK9Cvr + networks: + - yape-network + + antifraud-service: + build: + dockerfile: antifraud-yape/Dockerfile + context: . + depends_on: + - kafka + ports: + - 8081:8081 + networks: + - yape-network + + transaction-service: + build: + dockerfile: transaction-yape/Dockerfile + context: . + depends_on: + - kafka + - postgres + - cache + ports: + - 8080:8080 + networks: + - yape-network + +networks: + yape-network: \ No newline at end of file diff --git a/transaction-yape/.gitignore b/transaction-yape/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/transaction-yape/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/transaction-yape/Dockerfile b/transaction-yape/Dockerfile new file mode 100644 index 0000000..3038898 --- /dev/null +++ b/transaction-yape/Dockerfile @@ -0,0 +1,9 @@ +FROM gradle:jdk17 AS BUILD +WORKDIR /usr/app/ +COPY transaction-yape . +RUN gradle build + +FROM eclipse-temurin:17 +WORKDIR /usr/app/ +COPY --from=BUILD /usr/app/ . +ENTRYPOINT ["java","-jar","build/libs/transaction-yape-0.0.1-SNAPSHOT.jar"] \ No newline at end of file diff --git a/transaction-yape/build.gradle b/transaction-yape/build.gradle new file mode 100644 index 0000000..bda82df --- /dev/null +++ b/transaction-yape/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.3.0' + id 'io.spring.dependency-management' version '1.1.5' +} + +group = 'com.yape' +version = '0.0.1-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-graphql' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.kafka:spring-kafka' + compileOnly 'org.projectlombok:lombok' + implementation 'org.postgresql:postgresql' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework:spring-webflux' + testImplementation 'org.springframework.graphql:spring-graphql-test' + testImplementation 'org.springframework.kafka:spring-kafka-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'com.zaxxer:HikariCP:5.1.0' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/transaction-yape/gradle/wrapper/gradle-wrapper.jar b/transaction-yape/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
    NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/transaction-yape/gradlew.bat b/transaction-yape/gradlew.bat new file mode 100644 index 0000000..7101f8e --- /dev/null +++ b/transaction-yape/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/transaction-yape/settings.gradle b/transaction-yape/settings.gradle new file mode 100644 index 0000000..85acdca --- /dev/null +++ b/transaction-yape/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'transaction-yape' diff --git a/transaction-yape/src/main/java/com/yape/transaction/TransactionYapeApplication.java b/transaction-yape/src/main/java/com/yape/transaction/TransactionYapeApplication.java new file mode 100644 index 0000000..a947b3c --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/TransactionYapeApplication.java @@ -0,0 +1,15 @@ +package com.yape.transaction; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; + +@SpringBootApplication +@EnableCaching +public class TransactionYapeApplication { + + public static void main(String[] args) { + SpringApplication.run(TransactionYapeApplication.class, args); + } + +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransactionStatus.java b/transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransactionStatus.java new file mode 100644 index 0000000..2d46e6e --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransactionStatus.java @@ -0,0 +1,7 @@ +package com.yape.transaction.entities.enums; + +public enum TransactionStatus { + PENDING, + APPROVED, + REJECTED; +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransactionType.java b/transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransactionType.java new file mode 100644 index 0000000..840a009 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransactionType.java @@ -0,0 +1,6 @@ +package com.yape.transaction.entities.enums; + +public enum TransactionType { + WITHDRAWAL, + DEPOSIT; +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransferType.java b/transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransferType.java new file mode 100644 index 0000000..ce5cc24 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/entities/enums/TransferType.java @@ -0,0 +1,15 @@ +package com.yape.transaction.entities.enums; + +import java.util.Arrays; +import java.util.Optional; + +public enum TransferType { + NATIONAL, + INTERNATIONAL; + + public static Optional valueOf(Integer value) { + return Arrays.stream(values()) + .filter(transferType -> transferType.ordinal() == value) + .findFirst(); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/entities/models/Transaction.java b/transaction-yape/src/main/java/com/yape/transaction/entities/models/Transaction.java new file mode 100644 index 0000000..ffe7328 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/entities/models/Transaction.java @@ -0,0 +1,31 @@ +package com.yape.transaction.entities.models; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.yape.transaction.entities.enums.TransactionStatus; +import com.yape.transaction.entities.enums.TransactionType; +import com.yape.transaction.entities.enums.TransferType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Transaction implements Serializable { + private Long id; + private UUID transactionExternalId; + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private Double value; + private TransferType transferType; + private TransactionType transactionType; + private TransactionStatus transactionStatus; + private LocalDateTime createAt; + private LocalDateTime updateAt; +} \ No newline at end of file diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/adapter/ExceptionGraphQLAdapter.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/adapter/ExceptionGraphQLAdapter.java new file mode 100644 index 0000000..bf5b235 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/adapter/ExceptionGraphQLAdapter.java @@ -0,0 +1,35 @@ +package com.yape.transaction.infrastructure.adapter; + +import com.yape.transaction.usecases.models.exception.TransactionNotFoundException; +import graphql.GraphQLError; +import graphql.GraphqlErrorBuilder; +import graphql.schema.DataFetchingEnvironment; +import jakarta.validation.ConstraintViolationException; +import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter; +import org.springframework.graphql.execution.ErrorType; +import org.springframework.stereotype.Component; + +@Component +public class ExceptionGraphQLAdapter extends DataFetcherExceptionResolverAdapter { + + @Override + protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) { + if (ex instanceof TransactionNotFoundException) { + return GraphqlErrorBuilder.newError() + .errorType(ErrorType.NOT_FOUND) + .message(ex.getMessage()) + .path(env.getExecutionStepInfo().getPath()) + .location(env.getField().getSourceLocation()) + .build(); + } else if (ex instanceof ConstraintViolationException) { + return GraphqlErrorBuilder.newError() + .errorType(ErrorType.BAD_REQUEST) + .message(ex.getMessage()) + .path(env.getExecutionStepInfo().getPath()) + .location(env.getField().getSourceLocation()) + .build(); + } else { + return null; + } + } +} \ No newline at end of file diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/config/RedisConfig.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/config/RedisConfig.java new file mode 100644 index 0000000..72896fc --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/config/RedisConfig.java @@ -0,0 +1,54 @@ +package com.yape.transaction.infrastructure.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.yape.transaction.entities.models.Transaction; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@Configuration +@EnableCaching +public class RedisConfig { + + @Value("${spring.data.redis.ttl}") + private Integer tll; + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + + ObjectMapper objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(objectMapper, Transaction.class); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(jackson2JsonRedisSerializer); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(jackson2JsonRedisSerializer); + + template.setConnectionFactory(redisConnectionFactory); + + return template; + } + + @Bean + public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { + RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofSeconds(tll)) + .disableCachingNullValues(); + return RedisCacheManager.builder(redisConnectionFactory) + .cacheDefaults(cacheConfiguration) + .build(); + } +} \ No newline at end of file diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionGraphQL.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionGraphQL.java new file mode 100644 index 0000000..087d33e --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionGraphQL.java @@ -0,0 +1,43 @@ +package com.yape.transaction.infrastructure.in; + +import com.yape.transaction.usecases.in.CreateTransactionInputBoundary; +import com.yape.transaction.usecases.in.GetTransactionInputBoundary; +import com.yape.transaction.entities.models.Transaction; +import com.yape.transaction.infrastructure.models.CreateTransactionRequestModel; +import com.yape.transaction.infrastructure.models.GetTransactionRequestModel; +import com.yape.transaction.infrastructure.models.GetTransactionResponseModel; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; + +@Slf4j +@Controller +public class TransactionGraphQL { + + private final CreateTransactionInputBoundary createTransactionInputBoundary; + private final GetTransactionInputBoundary getTransactionInputBoundary; + + public TransactionGraphQL(CreateTransactionInputBoundary createTransactionInputBoundary, + GetTransactionInputBoundary getTransactionInputBoundary) { + this.createTransactionInputBoundary = createTransactionInputBoundary; + this.getTransactionInputBoundary = getTransactionInputBoundary; + } + + @QueryMapping + public GetTransactionResponseModel getTransaction(@Argument @Valid GetTransactionRequestModel getTransactionRequestModel) { + log.info("TransactionGraphQL getTransaction getTransactionRequestModel {}", getTransactionRequestModel); + Transaction transaction = getTransactionInputBoundary.getTransaction(getTransactionRequestModel.getUseCaseModel()); + return GetTransactionResponseModel.convertFrom(transaction); + + } + + @MutationMapping + public GetTransactionResponseModel createTransaction(@Argument @Valid CreateTransactionRequestModel createTransactionRequestModel) { + log.info("TransactionGraphQL createTransaction getTransactionRequestModel {}", createTransactionRequestModel); + Transaction transaction = createTransactionInputBoundary.createTransaction(createTransactionRequestModel.getUseCaseModel()); + return GetTransactionResponseModel.convertFrom(transaction); + } +} \ No newline at end of file diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionKafkaConsumer.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionKafkaConsumer.java new file mode 100644 index 0000000..56a2341 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionKafkaConsumer.java @@ -0,0 +1,23 @@ +package com.yape.transaction.infrastructure.in; + +import com.yape.transaction.usecases.in.UpdateTransactionInputBoundary; +import com.yape.transaction.infrastructure.models.TransactionMessageModel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class TransactionKafkaConsumer { + private final UpdateTransactionInputBoundary updateTransactionInputBoundary; + + public TransactionKafkaConsumer(UpdateTransactionInputBoundary updateTransactionInputBoundary) { + this.updateTransactionInputBoundary = updateTransactionInputBoundary; + } + + @KafkaListener(topics = "${spring.kafka.topic.transaction-update}", groupId = "${spring.kafka.consumer.group-id}") + public void updateTransaction(TransactionMessageModel transactionMessageModel) { + log.info("TransactionKafkaConsumer updateTransaction transactionMessageModel {}", transactionMessageModel); + updateTransactionInputBoundary.updateTransaction(transactionMessageModel.getUseCaseModel()); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionRestController.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionRestController.java new file mode 100644 index 0000000..8bf0a7d --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/in/TransactionRestController.java @@ -0,0 +1,63 @@ +package com.yape.transaction.infrastructure.in; + +import com.yape.transaction.usecases.in.CreateTransactionInputBoundary; +import com.yape.transaction.usecases.in.GetTransactionInputBoundary; +import com.yape.transaction.usecases.in.UpdateTransactionInputBoundary; +import com.yape.transaction.usecases.models.UpdateTransactionModel; +import com.yape.transaction.entities.models.Transaction; +import com.yape.transaction.infrastructure.models.CreateTransactionRequestModel; +import com.yape.transaction.infrastructure.models.GetTransactionRequestModel; +import com.yape.transaction.infrastructure.models.GetTransactionResponseModel; +import com.yape.transaction.infrastructure.models.UpdateTransactionRequestModel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@Slf4j +@RestController +public class TransactionRestController { + + private final CreateTransactionInputBoundary createTransactionInputBoundary; + private final GetTransactionInputBoundary getTransactionInputBoundary; + private final UpdateTransactionInputBoundary updateTransactionInputBoundary; + + public TransactionRestController(CreateTransactionInputBoundary createTransactionInputBoundary, + GetTransactionInputBoundary getTransactionInputBoundary, + UpdateTransactionInputBoundary updateTransactionInputBoundary) { + this.createTransactionInputBoundary = createTransactionInputBoundary; + this.getTransactionInputBoundary = getTransactionInputBoundary; + this.updateTransactionInputBoundary = updateTransactionInputBoundary; + } + + @GetMapping("/transaction/{transactionExternalId}") + public GetTransactionResponseModel getTransaction(@PathVariable String transactionExternalId) { + log.info("TransactionRestController GetTransactionResponseModel transactionExternalId {}", transactionExternalId); + GetTransactionRequestModel getTransactionRequestModel = GetTransactionRequestModel.builder() + .transactionExternalId(transactionExternalId) + .build(); + Transaction transaction = getTransactionInputBoundary.getTransaction(getTransactionRequestModel.getUseCaseModel()); + return GetTransactionResponseModel.convertFrom(transaction); + } + + @PostMapping("/transaction") + @ResponseStatus(HttpStatus.CREATED) + public GetTransactionResponseModel createTransaction(@RequestBody CreateTransactionRequestModel createTransactionRequestModel) { + log.info("TransactionRestController createTransaction createTransactionRequestModel {}", createTransactionRequestModel); + Transaction transaction = createTransactionInputBoundary.createTransaction(createTransactionRequestModel.getUseCaseModel()); + return GetTransactionResponseModel.convertFrom(transaction); + } + + @PatchMapping("/transaction/{transactionExternalId}") + public GetTransactionResponseModel updateStatusTransaction( + @RequestBody UpdateTransactionRequestModel updateTransactionRequestModel, + @PathVariable("transactionExternalId") String transactionExternalId) { + log.info("TransactionRestController updateStatusTransaction updateTransactionRequestModel {} transactionExternalId {}", + updateTransactionRequestModel, transactionExternalId); + UpdateTransactionModel updateTransactionModel = updateTransactionRequestModel.getUseCaseModel(); + updateTransactionModel.setTransactionExternalId(UUID.fromString(transactionExternalId)); + Transaction transaction = updateTransactionInputBoundary.updateTransaction(updateTransactionModel); + return GetTransactionResponseModel.convertFrom(transaction); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/AntifraudMessageModel.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/AntifraudMessageModel.java new file mode 100644 index 0000000..7a27a16 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/AntifraudMessageModel.java @@ -0,0 +1,23 @@ +package com.yape.transaction.infrastructure.models; + +import com.yape.transaction.entities.models.Transaction; +import lombok.Builder; +import lombok.Data; + +import java.io.Serializable; + +@Data +@Builder +public class AntifraudMessageModel implements Serializable { + private String transactionExternalId; + private Double value; + private String status; + + public static AntifraudMessageModel getInstanceFrom(Transaction transaction) { + return AntifraudMessageModel.builder() + .transactionExternalId(transaction.getTransactionExternalId().toString()) + .status(transaction.getTransactionStatus().name()) + .value(transaction.getValue()) + .build(); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/CreateTransactionRequestModel.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/CreateTransactionRequestModel.java new file mode 100644 index 0000000..dd9122e --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/CreateTransactionRequestModel.java @@ -0,0 +1,42 @@ +package com.yape.transaction.infrastructure.models; + +import com.yape.transaction.usecases.models.CreateTransactionModel; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Pattern; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Data +@NoArgsConstructor +public class CreateTransactionRequestModel { + + @Pattern(regexp="^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", message = "Not a valid UUID") + @NotEmpty(message = "Must not be empty") + private String accountExternalIdDebit; + + @Pattern(regexp="^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", message = "Not a valid UUID") + @NotEmpty(message = "Must not be empty") + private String accountExternalIdCredit; + + @NotNull(message = "Must not be empty") + @Min(value = 0L, message = "The value must be positive") + private Double value; + + @NotNull(message = "Must not be empty") + private Integer transferTypeId; + + public CreateTransactionModel getUseCaseModel() { + return CreateTransactionModel.builder() + .accountExternalIdDebit(UUID.fromString(accountExternalIdDebit)) + .accountExternalIdCredit(UUID.fromString(accountExternalIdCredit)) + .value(value) + .transferTypeId(transferTypeId) + .build(); + } + + +} \ No newline at end of file diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/GetTransactionRequestModel.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/GetTransactionRequestModel.java new file mode 100644 index 0000000..715fff4 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/GetTransactionRequestModel.java @@ -0,0 +1,27 @@ +package com.yape.transaction.infrastructure.models; + +import com.yape.transaction.usecases.models.GetTransactionModel; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor + +public class GetTransactionRequestModel { + + @Pattern(regexp="^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", message = "Not a valid UUID") + @NotEmpty(message = "Must not be empty") + private String transactionExternalId; + + public GetTransactionModel getUseCaseModel() { + return GetTransactionModel.builder().transactionExternalId(UUID.fromString(transactionExternalId)).build(); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/GetTransactionResponseModel.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/GetTransactionResponseModel.java new file mode 100644 index 0000000..166282c --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/GetTransactionResponseModel.java @@ -0,0 +1,40 @@ +package com.yape.transaction.infrastructure.models; + +import com.yape.transaction.entities.models.Transaction; +import lombok.Builder; +import lombok.Data; + +import java.util.UUID; + +@Data +@Builder +public class GetTransactionResponseModel { + + private UUID transactionExternalId; + private Double value; + private String createdAt; + private TransactionType transactionType; + private TransactionStatus transactionStatus; + @Data + @Builder + static class TransactionType { + private String name; + } + @Data + @Builder + static class TransactionStatus { + private String name; + } + + public static GetTransactionResponseModel convertFrom(Transaction transaction) { + return GetTransactionResponseModel.builder() + .transactionExternalId(transaction.getTransactionExternalId()) + .value(transaction.getValue()) + .createdAt(transaction.getCreateAt() == null ? null : transaction.getCreateAt().toString()) + .transactionType(transaction.getTransactionType() == null ? null : + TransactionType.builder().name(transaction.getTransactionType().name()).build()) + .transactionStatus(transaction.getTransactionStatus() == null ? null : + TransactionStatus.builder().name(transaction.getTransactionStatus().name()).build()). + build(); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/TransactionMessageModel.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/TransactionMessageModel.java new file mode 100644 index 0000000..8cb002c --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/TransactionMessageModel.java @@ -0,0 +1,25 @@ +package com.yape.transaction.infrastructure.models; + +import com.yape.transaction.usecases.models.UpdateTransactionModel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.UUID; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class TransactionMessageModel { + private String transactionExternalId; + private String status; + + public UpdateTransactionModel getUseCaseModel() { + return UpdateTransactionModel.builder() + .transactionExternalId(UUID.fromString(transactionExternalId)) + .status(status) + .build(); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/UpdateTransactionRequestModel.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/UpdateTransactionRequestModel.java new file mode 100644 index 0000000..d56197e --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/UpdateTransactionRequestModel.java @@ -0,0 +1,15 @@ +package com.yape.transaction.infrastructure.models; + +import com.yape.transaction.usecases.models.UpdateTransactionModel; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class UpdateTransactionRequestModel { + private String status; + + public UpdateTransactionModel getUseCaseModel() { + return UpdateTransactionModel.builder().status(status).build(); + } +} \ No newline at end of file diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/persistance/TransactionRepository.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/persistance/TransactionRepository.java new file mode 100644 index 0000000..ea24ecd --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/persistance/TransactionRepository.java @@ -0,0 +1,11 @@ +package com.yape.transaction.infrastructure.models.persistance; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface TransactionRepository extends JpaRepository { + + TransactionSchema findByTransactionExternalId(UUID transactionExternalId); + +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/persistance/TransactionSchema.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/persistance/TransactionSchema.java new file mode 100644 index 0000000..5636440 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/models/persistance/TransactionSchema.java @@ -0,0 +1,80 @@ +package com.yape.transaction.infrastructure.models.persistance; + +import com.yape.transaction.entities.enums.TransactionStatus; +import com.yape.transaction.entities.enums.TransactionType; +import com.yape.transaction.entities.enums.TransferType; +import com.yape.transaction.entities.models.Transaction; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity(name = "transaction") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TransactionSchema { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name ="transaction_external_id") + private UUID transactionExternalId; + + @Column(name = "account_external_id_credit") + private UUID accountExternalIdCredit; + + @Column(name = "account_external_id_debit") + private UUID accountExternalIdDebit; + + @Column(name = "transaction_status") + private String transactionStatus; + + @Column(name = "transaction_type") + private String transactionType; + + private Double value; + + @Column(name = "transfer_type") + private String transferType; + + @Column(name = "created_at") + private LocalDateTime createAt; + + @Column(name = "updated_at") + private LocalDateTime updateAt; + + public static TransactionSchema convertFrom(Transaction transaction) { + return TransactionSchema.builder() + .id(transaction.getId()) + .accountExternalIdCredit(transaction.getAccountExternalIdCredit()) + .accountExternalIdDebit(transaction.getAccountExternalIdDebit()) + .transactionExternalId(transaction.getTransactionExternalId()) + .transactionStatus(transaction.getTransactionStatus() == null ? null : transaction.getTransactionStatus().name()) + .transactionType(transaction.getTransactionType() == null ? null : transaction.getTransactionType().name()) + .value(transaction.getValue()) + .transferType(transaction.getTransferType() == null ? null : transaction.getTransferType().name()) + .createAt(transaction.getCreateAt()) + .updateAt(transaction.getUpdateAt()) + .build(); + } + + public Transaction getEntity() { + return Transaction.builder() + .id(this.getId()) + .accountExternalIdCredit(this.getAccountExternalIdCredit()) + .transactionExternalId(this.getTransactionExternalId()) + .accountExternalIdDebit(this.getAccountExternalIdDebit()) + .transactionStatus(this.getTransactionStatus() == null ? null : TransactionStatus.valueOf(this.getTransactionStatus())) + .value(this.getValue()) + .transactionType(this.getTransactionType() == null ? null : TransactionType.valueOf(this.getTransactionType())) + .transferType(this.getTransferType() == null ? null : TransferType.valueOf(this.getTransferType())) + .createAt(this.getCreateAt()) + .build(); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/out/AntifraudKafkaGateway.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/out/AntifraudKafkaGateway.java new file mode 100644 index 0000000..6ffca6e --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/out/AntifraudKafkaGateway.java @@ -0,0 +1,29 @@ +package com.yape.transaction.infrastructure.out; + +import com.yape.transaction.usecases.out.AntifraudGateway; +import com.yape.transaction.entities.models.Transaction; +import com.yape.transaction.infrastructure.models.AntifraudMessageModel; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class AntifraudKafkaGateway implements AntifraudGateway { + + @Value("${spring.kafka.topic.antifraud-validate}") + private String kafkaTopicAntifraud; + + private final KafkaTemplate kafkaTemplate; + + public AntifraudKafkaGateway(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Override + public void validate(Transaction transaction) { + log.info("AntifraudKafkaGateway validate transaction {}", transaction); + kafkaTemplate.send(kafkaTopicAntifraud, AntifraudMessageModel.getInstanceFrom(transaction)); + } +} \ No newline at end of file diff --git a/transaction-yape/src/main/java/com/yape/transaction/infrastructure/out/TransactionJpaGateway.java b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/out/TransactionJpaGateway.java new file mode 100644 index 0000000..c0de53b --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/infrastructure/out/TransactionJpaGateway.java @@ -0,0 +1,48 @@ +package com.yape.transaction.infrastructure.out; + +import com.yape.transaction.usecases.out.TransactionDataAccessGateway; +import com.yape.transaction.entities.models.Transaction; +import com.yape.transaction.infrastructure.models.persistance.TransactionRepository; +import com.yape.transaction.infrastructure.models.persistance.TransactionSchema; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheConfig; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@CacheConfig(cacheNames = "transactionCache") +public class TransactionJpaGateway implements TransactionDataAccessGateway { + + public TransactionRepository transactionRepository; + + public TransactionJpaGateway(TransactionRepository transactionRepository) { + this.transactionRepository = transactionRepository; + } + + @Override + @CachePut(value = "transactions", key = "#transaction.transactionExternalId") + public Transaction create(Transaction transaction) { + log.info("TransactionJpaGateway create transaction {}", transaction); + return transactionRepository.save(TransactionSchema.convertFrom(transaction)).getEntity(); + } + + @Override + @CachePut(value = "transactions", key = "#transaction.transactionExternalId") + public Transaction update(Transaction transaction) { + log.info("TransactionJpaGateway update transaction {}", transaction); + return transactionRepository.save(TransactionSchema.convertFrom(transaction)).getEntity(); + } + + @Override + @Cacheable(value = "transactions", key = "#transaction.transactionExternalId", unless="#result == null") + public Transaction findByTransactionExternalId(Transaction transaction) { + log.info("TransactionJpaGateway findByTransactionExternalId transaction {}", transaction); + TransactionSchema transactionSchema = transactionRepository.findByTransactionExternalId(transaction.getTransactionExternalId()); + if (transactionSchema != null) { + return transactionSchema.getEntity(); + } + return null; + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/CreateTransactionUseCase.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/CreateTransactionUseCase.java new file mode 100644 index 0000000..9dd97bd --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/CreateTransactionUseCase.java @@ -0,0 +1,41 @@ +package com.yape.transaction.usecases; + +import com.yape.transaction.usecases.in.CreateTransactionInputBoundary; +import com.yape.transaction.usecases.models.CreateTransactionModel; +import com.yape.transaction.usecases.out.AntifraudGateway; +import com.yape.transaction.entities.enums.TransactionStatus; +import com.yape.transaction.entities.enums.TransactionType; +import com.yape.transaction.usecases.out.TransactionDataAccessGateway; +import com.yape.transaction.entities.models.Transaction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Slf4j +@Service +public class CreateTransactionUseCase implements CreateTransactionInputBoundary { + + private final TransactionDataAccessGateway transactionDataAccessGateway; + private final AntifraudGateway antifraudGateway; + + public CreateTransactionUseCase(TransactionDataAccessGateway transactionDataAccessGateway, AntifraudGateway antifraudGateway) { + this.transactionDataAccessGateway = transactionDataAccessGateway; + this.antifraudGateway = antifraudGateway; + } + + @Override + @Transactional + public Transaction createTransaction(CreateTransactionModel createTransactionModel) { + log.info("CreateTransactionUseCase createTransaction createTransactionModel {}", createTransactionModel); + Transaction transaction = createTransactionModel.convertEntity(); + transaction.setTransactionStatus(TransactionStatus.PENDING); + transaction.setTransactionType(TransactionType.DEPOSIT); + transaction.setTransactionExternalId(UUID.randomUUID()); + transaction.setCreateAt(LocalDateTime.now()); + antifraudGateway.validate(transaction); + return transactionDataAccessGateway.create(transaction); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/GetTransactionUseCase.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/GetTransactionUseCase.java new file mode 100644 index 0000000..e9c35ac --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/GetTransactionUseCase.java @@ -0,0 +1,30 @@ +package com.yape.transaction.usecases; + +import com.yape.transaction.usecases.models.exception.TransactionNotFoundException; +import com.yape.transaction.usecases.in.GetTransactionInputBoundary; +import com.yape.transaction.usecases.models.GetTransactionModel; +import com.yape.transaction.usecases.out.TransactionDataAccessGateway; +import com.yape.transaction.entities.models.Transaction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class GetTransactionUseCase implements GetTransactionInputBoundary { + + private final TransactionDataAccessGateway transactionDataAccessGateway; + + public GetTransactionUseCase(TransactionDataAccessGateway transactionDataAccessGateway) { + this.transactionDataAccessGateway = transactionDataAccessGateway; + } + + @Override + public Transaction getTransaction(GetTransactionModel getTransactionModel) { + log.info("GetTransactionUseCase getTransaction getTransactionModel {}", getTransactionModel); + Transaction transaction = transactionDataAccessGateway.findByTransactionExternalId(getTransactionModel.convertEntity()); + if (transaction == null) { + throw new TransactionNotFoundException("Unable to find transaction with given id"); + } + return transaction; + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/UpdateTransactionUseCase.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/UpdateTransactionUseCase.java new file mode 100644 index 0000000..9217b6b --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/UpdateTransactionUseCase.java @@ -0,0 +1,37 @@ +package com.yape.transaction.usecases; + +import com.yape.transaction.usecases.in.UpdateTransactionInputBoundary; +import com.yape.transaction.usecases.models.UpdateTransactionModel; +import com.yape.transaction.usecases.models.exception.TransactionNotFoundException; +import com.yape.transaction.usecases.out.TransactionDataAccessGateway; +import com.yape.transaction.entities.enums.TransactionStatus; +import com.yape.transaction.entities.models.Transaction; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@Slf4j +@Service +public class UpdateTransactionUseCase implements UpdateTransactionInputBoundary { + + private final TransactionDataAccessGateway transactionDataAccessGateway; + + public UpdateTransactionUseCase(TransactionDataAccessGateway transactionDataAccessGateway) { + this.transactionDataAccessGateway = transactionDataAccessGateway; + } + + @Override + @Transactional + public Transaction updateTransaction(UpdateTransactionModel updateTransactionModel) { + log.info("UpdateTransactionUseCase updateTransaction updateTransactionModel {}", updateTransactionModel); + Transaction transaction = transactionDataAccessGateway.findByTransactionExternalId(updateTransactionModel.convertEntity()); + if (transaction == null) { + throw new TransactionNotFoundException("Unable to find transaction with given id"); + } + transaction.setTransactionStatus(TransactionStatus.valueOf(updateTransactionModel.getStatus())); + transaction.setUpdateAt(LocalDateTime.now()); + return transactionDataAccessGateway.update(transaction); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/in/CreateTransactionInputBoundary.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/in/CreateTransactionInputBoundary.java new file mode 100644 index 0000000..cc31cf8 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/in/CreateTransactionInputBoundary.java @@ -0,0 +1,8 @@ +package com.yape.transaction.usecases.in; + +import com.yape.transaction.usecases.models.CreateTransactionModel; +import com.yape.transaction.entities.models.Transaction; + +public interface CreateTransactionInputBoundary { + Transaction createTransaction(CreateTransactionModel createTransactionModel); +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/in/GetTransactionInputBoundary.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/in/GetTransactionInputBoundary.java new file mode 100644 index 0000000..68297f2 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/in/GetTransactionInputBoundary.java @@ -0,0 +1,8 @@ +package com.yape.transaction.usecases.in; + +import com.yape.transaction.usecases.models.GetTransactionModel; +import com.yape.transaction.entities.models.Transaction; + +public interface GetTransactionInputBoundary { + Transaction getTransaction(GetTransactionModel getTransactionModel); +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/in/UpdateTransactionInputBoundary.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/in/UpdateTransactionInputBoundary.java new file mode 100644 index 0000000..529d359 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/in/UpdateTransactionInputBoundary.java @@ -0,0 +1,8 @@ +package com.yape.transaction.usecases.in; + +import com.yape.transaction.usecases.models.UpdateTransactionModel; +import com.yape.transaction.entities.models.Transaction; + +public interface UpdateTransactionInputBoundary { + Transaction updateTransaction(UpdateTransactionModel updateTransactionModel); +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/models/CreateTransactionModel.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/models/CreateTransactionModel.java new file mode 100644 index 0000000..f7cc203 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/models/CreateTransactionModel.java @@ -0,0 +1,26 @@ +package com.yape.transaction.usecases.models; + +import com.yape.transaction.entities.enums.TransferType; +import com.yape.transaction.entities.models.Transaction; +import lombok.Builder; +import lombok.Data; + +import java.util.UUID; + +@Data +@Builder +public class CreateTransactionModel { + UUID accountExternalIdDebit; + UUID accountExternalIdCredit; + Double value; + Integer transferTypeId; + + public Transaction convertEntity() { + return Transaction.builder() + .accountExternalIdDebit(this.getAccountExternalIdDebit()) + .accountExternalIdCredit(this.getAccountExternalIdCredit()) + .value(this.getValue()) + .transferType(TransferType.valueOf(this.getTransferTypeId()).orElse(null)) + .build(); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/models/GetTransactionModel.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/models/GetTransactionModel.java new file mode 100644 index 0000000..0359a1d --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/models/GetTransactionModel.java @@ -0,0 +1,19 @@ +package com.yape.transaction.usecases.models; + +import com.yape.transaction.entities.models.Transaction; +import lombok.Builder; +import lombok.Data; + +import java.util.UUID; + +@Data +@Builder +public class GetTransactionModel { + UUID transactionExternalId; + + public Transaction convertEntity() { + return Transaction.builder() + .transactionExternalId(this.getTransactionExternalId()) + .build(); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/models/UpdateTransactionModel.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/models/UpdateTransactionModel.java new file mode 100644 index 0000000..84aa475 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/models/UpdateTransactionModel.java @@ -0,0 +1,22 @@ +package com.yape.transaction.usecases.models; + +import com.yape.transaction.entities.enums.TransactionStatus; +import com.yape.transaction.entities.models.Transaction; +import lombok.Builder; +import lombok.Data; + +import java.util.UUID; + +@Data +@Builder +public class UpdateTransactionModel { + private UUID transactionExternalId; + private String status; + + public Transaction convertEntity() { + return Transaction.builder() + .transactionExternalId(this.getTransactionExternalId()) + .transactionStatus(TransactionStatus.valueOf(this.getStatus())) + .build(); + } +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/models/exception/TransactionNotFoundException.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/models/exception/TransactionNotFoundException.java new file mode 100644 index 0000000..35c0d23 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/models/exception/TransactionNotFoundException.java @@ -0,0 +1,7 @@ +package com.yape.transaction.usecases.models.exception; + +public class TransactionNotFoundException extends RuntimeException { + public TransactionNotFoundException(String message) { + super(message); + } +} \ No newline at end of file diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/out/AntifraudGateway.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/out/AntifraudGateway.java new file mode 100644 index 0000000..829e3df --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/out/AntifraudGateway.java @@ -0,0 +1,7 @@ +package com.yape.transaction.usecases.out; + +import com.yape.transaction.entities.models.Transaction; + +public interface AntifraudGateway { + void validate(Transaction transaction); +} diff --git a/transaction-yape/src/main/java/com/yape/transaction/usecases/out/TransactionDataAccessGateway.java b/transaction-yape/src/main/java/com/yape/transaction/usecases/out/TransactionDataAccessGateway.java new file mode 100644 index 0000000..b41ea00 --- /dev/null +++ b/transaction-yape/src/main/java/com/yape/transaction/usecases/out/TransactionDataAccessGateway.java @@ -0,0 +1,9 @@ +package com.yape.transaction.usecases.out; + +import com.yape.transaction.entities.models.Transaction; + +public interface TransactionDataAccessGateway { + Transaction create(Transaction transaction); + Transaction update(Transaction transaction); + Transaction findByTransactionExternalId(Transaction transaction); +} diff --git a/transaction-yape/src/main/resources/application.yml b/transaction-yape/src/main/resources/application.yml new file mode 100644 index 0000000..a24cd19 --- /dev/null +++ b/transaction-yape/src/main/resources/application.yml @@ -0,0 +1,69 @@ +spring: + application: + name: transaction-yape + + datasource: + #url: jdbc:postgresql://localhost:5432/postgres + url: jdbc:postgresql://postgres:5432/postgres + username: postgres + password: postgres + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 10 + minimum-idle: 5 + idle-timeout: 30000 + max-lifetime: 1800000 + connection-timeout: 30000 + + jpa: + show-sql: true + hibernate: + ddl-auto: create-drop #dev + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + + graphql: + graphiql: + enabled: true + path: graphiql + + + kafka: + #bootstrap-servers: localhost:9092 + bootstrap-servers: kafka:29092 + topic: + antifraud-validate: antifraud + transaction-update: transaction + producer: + key-serializer: org.springframework.kafka.support.serializer.JsonSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + + consumer: + group-id: transaction-group + auto-offset-reset: earliest + key-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring: + json: + type: + mapping: 'com.yape.antifraud.infrastructure.models.TransactionMessageModel:com.yape.transaction.infrastructure.models.TransactionMessageModel' + trusted: + packages: 'com.yape.antifraud.infrastructure.models' + + listener: + missing-topics-fatal: false + + + data: + redis: + host: cache + #host: localhost + port: 6379 + password: jA29i4X5mt9QK9Cvr + ttl: 60 + +logging: + level: + org.springframework.cache: TRACE \ No newline at end of file diff --git a/transaction-yape/src/main/resources/graphql/transaction.graphqls b/transaction-yape/src/main/resources/graphql/transaction.graphqls new file mode 100644 index 0000000..3406523 --- /dev/null +++ b/transaction-yape/src/main/resources/graphql/transaction.graphqls @@ -0,0 +1,34 @@ +type GetTransactionResponseModel { + transactionExternalId: String + transactionType: TransactionType + transactionStatus: TransactionStatus + value: Float + createdAt: String +} + +type TransactionType { + name: String +} + +type TransactionStatus { + name: String +} + +input CreateTransactionRequestModel { + transferTypeId: Int + value: Float + accountExternalIdDebit: String + accountExternalIdCredit: String +} + +input GetTransactionRequestModel { + transactionExternalId: String +} + +type Query { + getTransaction(getTransactionRequestModel : GetTransactionRequestModel): GetTransactionResponseModel +} + +type Mutation { + createTransaction(createTransactionRequestModel : CreateTransactionRequestModel): GetTransactionResponseModel +} \ No newline at end of file diff --git a/transaction-yape/src/test/java/com/yape/transaction/TransactionYapeApplicationTests.java b/transaction-yape/src/test/java/com/yape/transaction/TransactionYapeApplicationTests.java new file mode 100644 index 0000000..9c82629 --- /dev/null +++ b/transaction-yape/src/test/java/com/yape/transaction/TransactionYapeApplicationTests.java @@ -0,0 +1,11 @@ +package com.yape.transaction; + +import org.junit.jupiter.api.Test; + +class TransactionYapeApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/transaction-yape/src/test/java/com/yape/transaction/usecases/CreateTransactionUseCaseTests.java b/transaction-yape/src/test/java/com/yape/transaction/usecases/CreateTransactionUseCaseTests.java new file mode 100644 index 0000000..e162bcb --- /dev/null +++ b/transaction-yape/src/test/java/com/yape/transaction/usecases/CreateTransactionUseCaseTests.java @@ -0,0 +1,55 @@ +package com.yape.transaction.usecases; + +import com.yape.transaction.entities.enums.TransferType; +import com.yape.transaction.entities.models.Transaction; +import com.yape.transaction.infrastructure.out.AntifraudKafkaGateway; +import com.yape.transaction.infrastructure.out.TransactionJpaGateway; +import com.yape.transaction.usecases.models.CreateTransactionModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@ExtendWith(MockitoExtension.class) +public class CreateTransactionUseCaseTests { + + @Mock + private TransactionJpaGateway transactionJpaGateway; + + @Mock + private AntifraudKafkaGateway antifraudKafkaGateway; + + private CreateTransactionUseCase createTransactionUseCase; + + @BeforeEach + public void beforeAll() { + createTransactionUseCase = new CreateTransactionUseCase(transactionJpaGateway, antifraudKafkaGateway); + } + + @DisplayName("Create transaction Successfully") + @Test + public void createTransactionSuccessfully(){ + + Mockito.when(transactionJpaGateway.create(Mockito.any())).thenReturn(Transaction + .builder() + .transactionExternalId(UUID.randomUUID()) + .build()); + + Transaction transaction = createTransactionUseCase.createTransaction(CreateTransactionModel.builder() + .value(1D) + .accountExternalIdCredit(UUID.randomUUID()) + .accountExternalIdDebit(UUID.randomUUID()) + .transferTypeId(TransferType.NATIONAL.ordinal()) + .build()); + + assertThat(transaction).isNotNull(); + assertThat(transaction.getTransactionExternalId()).isNotNull(); + } +} diff --git a/transaction-yape/src/test/java/com/yape/transaction/usecases/GetTransactionUseCaseTests.java b/transaction-yape/src/test/java/com/yape/transaction/usecases/GetTransactionUseCaseTests.java new file mode 100644 index 0000000..0128239 --- /dev/null +++ b/transaction-yape/src/test/java/com/yape/transaction/usecases/GetTransactionUseCaseTests.java @@ -0,0 +1,59 @@ +package com.yape.transaction.usecases; + +import com.yape.transaction.entities.models.Transaction; +import com.yape.transaction.infrastructure.out.TransactionJpaGateway; +import com.yape.transaction.usecases.models.GetTransactionModel; +import com.yape.transaction.usecases.models.exception.TransactionNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +public class GetTransactionUseCaseTests { + + @Mock + private TransactionJpaGateway transactionDataAccessGateway; + + private GetTransactionUseCase getTransactionUseCase; + + @BeforeEach + public void beforeAll() { + getTransactionUseCase = new GetTransactionUseCase(transactionDataAccessGateway); + } + + @DisplayName("Get transaction - Successfully") + @Test + public void getTransactionSuccessfully(){ + + Mockito.when(transactionDataAccessGateway.findByTransactionExternalId(Mockito.any())).thenReturn(Transaction.builder().build()); + + Transaction transaction = getTransactionUseCase.getTransaction(GetTransactionModel.builder() + .transactionExternalId(UUID.randomUUID()) + .build()); + + assertThat(transaction).isNotNull(); + } + + + @DisplayName("Get transaction - Not found") + @Test + public void getTransactionSuccessError_notFound(){ + + Mockito.when(transactionDataAccessGateway.findByTransactionExternalId(Mockito.any())).thenReturn(null); + + assertThrows(TransactionNotFoundException.class, () -> { + Transaction transaction = getTransactionUseCase.getTransaction(GetTransactionModel.builder() + .transactionExternalId(UUID.randomUUID()) + .build()); + }); + } +} diff --git a/transaction-yape/src/test/java/com/yape/transaction/usecases/UpdateTransactionUseCaseTests.java b/transaction-yape/src/test/java/com/yape/transaction/usecases/UpdateTransactionUseCaseTests.java new file mode 100644 index 0000000..70571df --- /dev/null +++ b/transaction-yape/src/test/java/com/yape/transaction/usecases/UpdateTransactionUseCaseTests.java @@ -0,0 +1,74 @@ +package com.yape.transaction.usecases; + +import com.yape.transaction.entities.enums.TransactionStatus; +import com.yape.transaction.entities.models.Transaction; +import com.yape.transaction.infrastructure.out.TransactionJpaGateway; +import com.yape.transaction.usecases.models.UpdateTransactionModel; +import com.yape.transaction.usecases.models.exception.TransactionNotFoundException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(MockitoExtension.class) +public class UpdateTransactionUseCaseTests { + + @Mock + private TransactionJpaGateway transactionDataAccessGateway; + + private UpdateTransactionUseCase updateTransactionUseCase; + + @BeforeEach + public void beforeAll() { + updateTransactionUseCase = new UpdateTransactionUseCase(transactionDataAccessGateway); + } + + @DisplayName("Update transaction - Successfully (APPROVED)") + @Test + public void updateTransactionSuccessfully(){ + + Mockito.when(transactionDataAccessGateway.findByTransactionExternalId(Mockito.any())).thenReturn( + Transaction.builder() + .transactionStatus(TransactionStatus.APPROVED) + .build() + ); + + Mockito.when(transactionDataAccessGateway.update(Mockito.any())).thenReturn( + Transaction.builder() + .transactionStatus(TransactionStatus.APPROVED) + .build() + ); + + Transaction transaction = updateTransactionUseCase.updateTransaction(UpdateTransactionModel.builder() + .transactionExternalId(UUID.randomUUID()) + .status(TransactionStatus.APPROVED.name()) + .build()); + + assertThat(transaction).isNotNull(); + assertEquals(transaction.getTransactionStatus().name(), TransactionStatus.APPROVED.name()); + } + + + @DisplayName("Update transaction - Not found") + @Test + public void getTransactionSuccessError_notFound(){ + + Mockito.when(transactionDataAccessGateway.findByTransactionExternalId(Mockito.any())).thenReturn(null); + + assertThrows(TransactionNotFoundException.class, () -> { + updateTransactionUseCase.updateTransaction(UpdateTransactionModel.builder() + .transactionExternalId(UUID.randomUUID()) + .status(TransactionStatus.APPROVED.name()) + .build()); + }); + } +} From 4b2dffbb6c7d36c8b0b5963112fdf6c5dc345227 Mon Sep 17 00:00:00 2001 From: Karl Renzo Date: Tue, 11 Jun 2024 23:34:33 -0500 Subject: [PATCH 2/2] Update README.md --- README.md | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 69399ab..be9873e 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,60 @@ # Yape Challenge - Karl Renzo Alcala Paucar -## Tech Stack - -**Server:** Spring boot, GraphQL, Kafka, Gradle, Postgres, Lombok, JUnit, HikariCP - - ## Description ### Arquitectura del Proyecto -Para la solucion a este problema se decidió implementar Clean Architecture el cual se encuentra definido en el libro Clean Architecture por Robert C. Martin, este tipo de arquitectura trata de englobar las arquitecturas previamente existentes tales como Hexagonal, Onion, etc. Y nos brinda ciertas ventajas tales como: facilidad para realizar tests, independencia de frameworks, independencia de la base de datos, independencia de cualquier agente externo y independencia de UI. +Para la solución a este problema se decidió implementar Clean Architecture el cual se encuentra definido en el libro Clean Architecture por Robert C. Martin, este tipo de arquitectura trata de englobar las arquitecturas previamente existentes tales como Hexagonal, Onion, etc. Y nos brinda ciertas ventajas tales como: facilidad para realizar tests, independencia de frameworks, independencia de la base de datos, independencia de cualquier agente externo y independencia de UI. + +Screenshot 2024-06-11 at 9 12 45 PM ### Estructura del Proyecto El proyecto ha sido dividido en 3 capas: -- Entities: en esta capa de almacenan todas las entidades de negocio (estos pueden ser simples estructuras de datos con metodos), son los componentes de mas alto nivel, debido a esto, son los que muy probablemente no cambien debido a cambios externos. -- Uses cases: en esta capa se encuentran las reglas de negocio de la aplicacion, esto significa que los componentes en esta capa se encargan de administrar las entidades de negocio y el flujo de los datos. -- Infrastructure: en esta capa tenemos todos los componentes de mas bajo nivel tales como componentes de base de datos, framework GraphQL, framework RestController, por los que estos componentes estan destinados como conexion con otras aplicaciones externos. +- Entities: en esta capa de almacenan todas las entidades de negocio (estos pueden ser simples estructuras de datos con métodos), son los componentes de más alto nivel, debido a esto, son los que muy probablemente no cambien debido a cambios externos. +- Uses cases: en esta capa se encuentran las reglas de negocio de la aplicación, esto significa que los componentes en esta capa se encargan de administrar las entidades de negocio y el flujo de los datos. +- Infrastructure: en esta capa tenemos todos los componentes de mas bajo nivel tales como componentes de base de datos, framework GraphQL, framework RestController, por los que estos componentes estan destinados como conexión con otras aplicaciones externas. + +### Componentes de la solución -### Componentes de la solucion +- Antifraud: este es un microservicio construido usando Spring Boot el cual tiene como objetivo validar el monto de las transacciones creadas en el componente Transaction, este componente es un Consumer Kafka y recibe los mensajes debido a que esta suscrito al topic "antifraud". Después de realizar la validación, mandara un mensaje al topic "transaction" para actualizar el estado de la transacción "APPROVED" or "REJECTED", esto lo logra debido a que también es un Producer kafka. -- Antifraud: este es un microservicio construido usando Spring Boot el cual tiene como objetivo validar el monto de las transacciones creadas en el componente Transaction, este componente es un Consumer Kafka y recibe los mensajes debido a que esta suscrito al topic "antifraud". Despues de realizar la validacion, mandara un mensaje al topic "transaction" para actualizar el estado de la transaccion "APPROVED" or "REJECTED", esto lo logra debido a que tambien es un Producer kafka. +- Transaction: este es un microservicio construido con Spring Boot el cual tiene como objetivo realizar distintas operaciones a las transacciones, tiene métodos para crear, modificar estado y obtener una transacción. Tiene una conexión a la base de datos Postgres para realizar dichas operaciones. Este microservicio es un Producer Kafka porque durante el proceso de crear una transacción, este componente envia un mensaje al topic "antifraud" para que el microservicio Antifraud valide el monto de la transacción. Además este microservicio se comporta como un Consumer para que pueda recibir las peticiones de modificación de estado que envia el componente "antifraud". -- Transaction: este es un microservicio construido con Spring Boot el cual tiene como objetivo realizar distintas operaciones a las transacciones, tiene metodos para crear, modificar estado y obtener una transaccion. Tiene una conexion a la base de datos Postgres para realizar dichas operaciones. Este microservicio es un Producer Kafka porque durante el proceso de crear una transaccion, este componente envia un mensaje al topic "antifraud" para que el microservicio Antifraud valide el monto de la transaccion. Ademas este microservicio se comporta como un Consumer para que pueda recibir las peticiones de modificacion de estado que envia el componente "antifraud". +- Kafka: este componente es utilizado para la comunicación entre el microservicio de Antifraud y Transaction. Además, utiliza 2 topics: "antifraud" y "transaction", el primero sirve para validar el monto de las transacciones, el consumer es el microservicio Antifraud, el segundo es utilizado para actualizar el estado de la transacción, el consumer de este topic es el microservicio de "Transaction". -- Kafka: este componente es utilizado para la comunicacion entre el microservicio de Antifraud y Transaction. Ademas, utiliza 2 topics: "antifraud" y "transaction", el primero sirve para validar el monto de las transacciones, el consumer es el microservicio Antifraud, el segundo es utilizado para actualizar el estado de la transaccion, el consumer de este topic es el microservicio de "Transaction". +- Postgres: es una base de datos relacional que servirá para persistir las transacciones, contendrá una sola tabla Transaction con todas las columnas necesarias para guardar la información de la transacción. El microservicio de Transaction se conectara a esta base de datos utilizando un pool de conexiones que sera administrada por el framework HikariCP. -- Postgres: es una base de datos relacional que servira para persistir las transacciones, contendra una sola tabla Transaction con todas las columnas necesarias para guardar la informacion de la transaccion. El microservicio de Transaction se coneectara a esta base de datos utilizando un pool de conexiones que sera administrada por el framework HikariCP. +- Redis: Componente que funcionará como cache para la operación de consultar transacciones. + +Screenshot 2024-06-11 at 8 45 29 PM -- Redis: Componente que funcionara como cache para la operacion de consultar transacciones. ### Endpoints Se cuentan con los siguientes recursos en GraphQL: - Mutation: createTransaction: Crear transaction y validarlo -- Query: getTransaction: Obtener una transaction por codigo +- Query: getTransaction: Obtener una transaction por código + +## Tech Stack + +**Server:** Spring boot, GraphQL, Kafka, Gradle, Postgres, Lombok, JUnit, HikariCP + ## Levantar proyecto localmente Clone the project ```bash - git clone + git clone https://github.com/RenzoAlcala/app-java-codechallenge.git ``` Go to the project directory ```bash - cd my-project + cd app-java-codechallenge ``` Start all the components @@ -74,3 +79,4 @@ Tener instalado docker. ## Autor - Karl Renzo Alcala Paucar +