diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..c445652 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c131f6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,68 @@ +### Intellij ### +antifraud-yape/.idea/ +antifraud-yape/.idea_modules/ + +# JIRA plugin +### Java ### +# Compiled class file +antifraud-yape/*.class + + +# Package Files # +antifraud-yape/*.jar +antifraud-yape/*.war + +### Gradle ### +antifraud-yape/.gradle +antifraud-yape/**/build/ +antifraud-yape/!src/**/build/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +antifraud-yape/!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +antifraud-yape/!gradle-wrapper.properties + +# Cache of project +antifraud-yape/.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +antifraud-yape/.project +# JDT-specific (Eclipse Java Development Tools) +antifraud-yape/.classpath + + +### Intellij ### +transaction-yape/.idea/ +transaction-yape/.idea_modules/ + +# JIRA plugin +### Java ### +# Compiled class file +transaction-yape/*.class + + +# Package Files # +transaction-yape/*.jar +transaction-yape/*.war + +### Gradle ### +transaction-yape/.gradle +transaction-yape/**/build/ +transaction-yape/!src/**/build/ + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +transaction-yape/!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +transaction-yape/!gradle-wrapper.properties + +# Cache of project +transaction-yape/.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +transaction-yape/.project +# JDT-specific (Eclipse Java Development Tools) +transaction-yape/.classpath diff --git a/README.md b/README.md index 7f832ad..be9873e 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,82 @@ -# Yape Code Challenge :rocket: -Our code challenge will let you marvel us with your Jedi coding skills :smile:. +# Yape Challenge - Karl Renzo Alcala Paucar -Don't forget that the proper way to submit your work is to fork the repo and create a PR :wink: ... have fun !! +## Description -- [Yape Code Challenge :rocket:](#yape-code-challenge-rocket) -- [Problem](#problem) -- [Tech Stack](#tech-stack) - - [Optional](#optional) -- [Send us your challenge](#send-us-your-challenge) +### Arquitectura del Proyecto -# Problem +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. -Every time a financial transaction is created it must be validated by our anti-fraud microservice and then the same service sends a message back to update the transaction status. -For now, we have only three transaction statuses: +Screenshot 2024-06-11 at 9 12 45 PM -
    -
  1. pending
  2. -
  3. approved
  4. -
  5. rejected
  6. -
+### Estructura del Proyecto +El proyecto ha sido dividido en 3 capas: -Every transaction with a value greater than 1000 should be rejected. +- 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. -```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)] -``` +### Componentes de la solución + +- 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. + +- 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". + +- 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". + +- 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. + +- Redis: Componente que funcionará como cache para la operación de consultar transacciones. + +Screenshot 2024-06-11 at 8 45 29 PM + + +### Endpoints + +Se cuentan con los siguientes recursos en GraphQL: -# Tech Stack +- Mutation: createTransaction: Crear transaction y validarlo +- Query: getTransaction: Obtener una transaction por código -
    -
  1. Java. You can use any framework you want
  2. -
  3. Any database
  4. -
  5. Kafka
  6. -
+## Tech Stack -We do provide a `Dockerfile` to help you get started with a dev environment. +**Server:** Spring boot, GraphQL, Kafka, Gradle, Postgres, Lombok, JUnit, HikariCP -You must have two resources: -1. Resource to create a transaction that must containt: +## Levantar proyecto localmente -```json -{ - "accountExternalIdDebit": "Guid", - "accountExternalIdCredit": "Guid", - "tranferTypeId": 1, - "value": 120 -} +Clone the project + +```bash + git clone https://github.com/RenzoAlcala/app-java-codechallenge.git ``` -2. Resource to retrieve a transaction - -```json -{ - "transactionExternalId": "Guid", - "transactionType": { - "name": "" - }, - "transactionStatus": { - "name": "" - }, - "value": 120, - "createdAt": "Date" -} +Go to the project directory + +```bash + cd app-java-codechallenge +``` + +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 +``` + +## Requisitos -You can use Graphql; +Tener instalado docker. -# Send us your challenge +## Autor -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. +- Karl Renzo Alcala Paucar -If you have any questions, please let us know. \ No newline at end of file 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 0000000..e644113 Binary files /dev/null and b/antifraud-yape/gradle/wrapper/gradle-wrapper.jar differ diff --git a/antifraud-yape/gradle/wrapper/gradle-wrapper.properties b/antifraud-yape/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a441313 --- /dev/null +++ b/antifraud-yape/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/antifraud-yape/gradlew b/antifraud-yape/gradlew new file mode 100755 index 0000000..b740cf1 --- /dev/null +++ b/antifraud-yape/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + 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 0000000..e644113 Binary files /dev/null and b/transaction-yape/gradle/wrapper/gradle-wrapper.jar differ diff --git a/transaction-yape/gradle/wrapper/gradle-wrapper.properties b/transaction-yape/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a441313 --- /dev/null +++ b/transaction-yape/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/transaction-yape/gradlew b/transaction-yape/gradlew new file mode 100755 index 0000000..b740cf1 --- /dev/null +++ b/transaction-yape/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + 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()); + }); + } +}