diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c43043a --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Directorios de Maven +target/ + +# Archivos de configuración locales +*.iml +.idea/ +*.classpath +*.project +*.settings/ +*.factorypath + +# Archivos de sistema +.DS_Store +Thumbs.db + +# Archivos de configuración de IntelliJ +*.ipr +*.iws +*.bak +*.swp + +# Archivos de log +*.log + +# Archivos de compilación +*.class + +# Archivos de compilación de Java +*.jar +*.war +*.ear + +# Configuración de sistema operativo +*.swp +.sass-cache/ +.vscode/ + +# Archivos específicos de ambiente +.env diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..4d33edb --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5157874 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/ENTREGABLE.md b/ENTREGABLE.md new file mode 100644 index 0000000..d19e20e --- /dev/null +++ b/ENTREGABLE.md @@ -0,0 +1,23 @@ +# API OPERACIONES-MS +Servicio que registra operaciones transactionales manejando eventos en diferentes topicos creados. Correctamente aunthenticado y autorizado para ejecución de cada recurso. + +# Tecnologia y Herramientas + +* Ide Intellij +* Spring Boot para creacion y configuracion de proyecto +* Desarrollo para Webflux +* MongoDB para persistencia reactiva. +* Estructura de Proyecto con modelo MVC +* Kafaka para gestion evento y topicos +* Spring Security para manejo de Authenticacion y Authorizacion +* JWT para manejo de token en validacion de metodos y creacion de usuarios. +* OpenAPi para documentacion y contratos de la api. +* JUnit5 para test de las clases utilizadas. + +# Entregas del Proyecto + +* Repositorio de GitHub con el código fuente +* Git https://github.com/Jemn/java-code-challenge/commits/main/ +* Postman para hacer pruebas de la api. /resource/collection-postman/Interbank.postman_collection.json +* Mongo db Json /resource/mongo-db/challenge_prod.table_role.json - challenge_prod.table_user.json - challenge_prod.table_transaction.json +* Documentacion /resource/documentacion/api-docs.yaml \ No newline at end of file diff --git a/HELP.md b/HELP.md new file mode 100644 index 0000000..e2f34be --- /dev/null +++ b/HELP.md @@ -0,0 +1,32 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.3.2/maven-plugin) +* [Create an OCI image](https://docs.spring.io/spring-boot/3.3.2/maven-plugin/build-image.html) +* [Spring Data JPA](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#data.sql.jpa-and-spring-data) +* [Validation](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#io.validation) +* [Spring Data Reactive MongoDB](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#data.nosql.mongodb) +* [Spring Reactive Web](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#web.reactive) +* [Spring Security](https://docs.spring.io/spring-boot/docs/3.3.2/reference/htmlsingle/index.html#web.security) + +### Guides +The following guides illustrate how to use some features concretely: + +* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) +* [Validation](https://spring.io/guides/gs/validating-form-input/) +* [Accessing Data with MongoDB](https://spring.io/guides/gs/accessing-data-mongodb/) +* [Building a Reactive RESTful Web Service](https://spring.io/guides/gs/reactive-rest-service/) +* [Securing a Web Application](https://spring.io/guides/gs/securing-web/) +* [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/) +* [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/) + +### Maven Parent overrides + +Due to Maven's design, elements are inherited from the parent POM to the project POM. +While most of the inheritance is fine, it also inherits unwanted elements like `` and `` from the parent. +To prevent this, the project POM contains empty overrides for these elements. +If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides. + diff --git a/docker-compose.yml b/docker-compose.yml index a59bedf..ff727cd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,19 +1,21 @@ version: "3.7" services: - postgres: - image: postgres:14 + mongo: + image: mongo:4.4 ports: - - "5432:5432" - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres + - "27017:27017" + volumes: + - mongo-data:/data/db + zookeeper: image: confluentinc/cp-zookeeper:5.5.3 environment: ZOOKEEPER_CLIENT_PORT: 2181 + kafka: image: confluentinc/cp-enterprise-kafka:5.5.3 - depends_on: [zookeeper] + depends_on: + - zookeeper environment: KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 @@ -23,6 +25,20 @@ services: KAFKA_JMX_PORT: 9991 ports: - 9092:9092 + + app: + image: api-operaciones-ms + build: + context: . + dockerfile: Dockerfile + depends_on: + - mongo + - kafka + environment: + - SPRING_DATA_MONGODB_URI=mongodb://mongo:27017/challenge_prod + - KAFKA_BOOTSTRAP_SERVERS=kafka:29092 + ports: + - "8083:8083" + volumes: - oracle-data: - oracle-backup: + mongo-data: diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..da19fb9 --- /dev/null +++ b/dockerfile @@ -0,0 +1,12 @@ +# Usa una imagen base de Maven para construir el proyecto +FROM maven:3.8.1-openjdk-17 AS build +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn clean package -DskipTests + +# Usa una imagen base de OpenJDK para ejecutar el proyecto +FROM openjdk:17-jdk-slim +WORKDIR /app +COPY --from=build /app/target/challenge-0.0.1-SNAPSHOT.jar app.jar +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..d7c358e --- /dev/null +++ b/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + 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" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..6f779cf --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0960cda --- /dev/null +++ b/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.2 + + + com.interbank + challenge + 0.0.1-SNAPSHOT + challenge + Demo project for Spring Boot + + 17 + + + + org.springframework.boot + spring-boot-starter-data-mongodb-reactive + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.kafka + spring-kafka + + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + org.springframework.security + spring-security-test + test + + + io.jsonwebtoken + jjwt + 0.9.1 + + + javax.xml.bind + jaxb-api + 2.3.1 + + + org.springdoc + springdoc-openapi-starter-webflux-api + 2.6.0 + + + org.mapstruct + mapstruct + 1.5.3.Final + + + org.mapstruct + mapstruct-processor + 1.5.3.Final + provided + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/interbank/challenge/ChallengeApplication.java b/src/main/java/com/interbank/challenge/ChallengeApplication.java new file mode 100644 index 0000000..469c359 --- /dev/null +++ b/src/main/java/com/interbank/challenge/ChallengeApplication.java @@ -0,0 +1,13 @@ +package com.interbank.challenge; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ChallengeApplication { + + public static void main(String[] args) { + SpringApplication.run(ChallengeApplication.class, args); + } + +} diff --git a/src/main/java/com/interbank/challenge/config/GlobalExceptionHandler.java b/src/main/java/com/interbank/challenge/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..0b8db4b --- /dev/null +++ b/src/main/java/com/interbank/challenge/config/GlobalExceptionHandler.java @@ -0,0 +1,25 @@ +package com.interbank.challenge.config; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.bind.support.WebExchangeBindException; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(WebExchangeBindException.class) + public Mono>> handleValidationExceptions(WebExchangeBindException ex) { + Map errors = new HashMap<>(); + for (FieldError error : ex.getFieldErrors()) { + errors.put(error.getField(), error.getDefaultMessage()); + } + return Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errors)); + } +} diff --git a/src/main/java/com/interbank/challenge/config/SecurityConfig.java b/src/main/java/com/interbank/challenge/config/SecurityConfig.java new file mode 100644 index 0000000..cc2e30b --- /dev/null +++ b/src/main/java/com/interbank/challenge/config/SecurityConfig.java @@ -0,0 +1,69 @@ +package com.interbank.challenge.config; + +import com.interbank.challenge.config.jwt.JwtFilter; +import com.interbank.challenge.config.jwt.JwtUtil; +import com.interbank.challenge.service.UserDetailsServiceImpl; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.security.authentication.ReactiveAuthenticationManager; +import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; +import org.springframework.security.config.web.server.SecurityWebFiltersOrder; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository; + +@Configuration +public class SecurityConfig { + + @Bean + public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http, JwtUtil jwtUtil, ReactiveUserDetailsService userDetailsService) { + JwtFilter jwtRequestFilter = new JwtFilter(jwtUtil, userDetailsService); + + http.csrf().disable() + .authorizeExchange() + .pathMatchers("/", + "/swagger-ui.html", + "/favicon.ico", + "swagger-ui/index.html", + "/swagger-ui/**", + "/v3/api-docs/**", + "/v3/api-docs.yaml", + "/api/login/signin", + "/api/login/signup", + "/api/transactions/save", + "/api/transactions/list/*").permitAll() + .anyExchange().authenticated() + .and() + .addFilterAt(jwtRequestFilter, SecurityWebFiltersOrder.AUTHENTICATION) + .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()); + + return http.build(); + } + + @Bean + @Primary + public ReactiveUserDetailsService userDetailsService(UserDetailsServiceImpl userDetailsService) { + return userDetailsService; + } + + @Bean + public ReactiveAuthenticationManager authenticationManager(ReactiveUserDetailsService userDetailsService) { + UserDetailsRepositoryReactiveAuthenticationManager authenticationManager = + new UserDetailsRepositoryReactiveAuthenticationManager(userDetailsService); + authenticationManager.setPasswordEncoder(passwordEncoder()); + return authenticationManager; + } + + @Bean + public BCryptPasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public JwtUtil jwtUtil() { + return new JwtUtil(); + } +} \ No newline at end of file diff --git a/src/main/java/com/interbank/challenge/config/jwt/JwtFilter.java b/src/main/java/com/interbank/challenge/config/jwt/JwtFilter.java new file mode 100644 index 0000000..ee3c08d --- /dev/null +++ b/src/main/java/com/interbank/challenge/config/jwt/JwtFilter.java @@ -0,0 +1,57 @@ +package com.interbank.challenge.config.jwt; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; +import reactor.util.context.Context; + +import java.util.List; + +@Component +public class JwtFilter implements WebFilter { + + private final JwtUtil jwtUtil; + private final ReactiveUserDetailsService userDetailsService; + + public JwtFilter(JwtUtil jwtUtil, ReactiveUserDetailsService userDetailsService) { + this.jwtUtil = jwtUtil; + this.userDetailsService = userDetailsService; + } + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + ServerHttpRequest request = exchange.getRequest(); + + final List authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION); + + if (authorizationHeader != null && !authorizationHeader.isEmpty() && authorizationHeader.get(0).startsWith("Bearer ")) { + String jwt = authorizationHeader.get(0).substring(7); + String username = jwtUtil.extractUsername(jwt); + + if (username != null) { + return userDetailsService.findByUsername(username) + .flatMap(userDetails -> { + if (jwtUtil.validateToken(jwt, userDetails.getUsername())) { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + userDetails, null, userDetails.getAuthorities()); + SecurityContext securityContext = new SecurityContextImpl(authentication); + + return chain.filter(exchange) + .contextWrite(Context.of(SecurityContext.class, securityContext)); + } else { + return chain.filter(exchange); + } + }); + } + } + return chain.filter(exchange); + } +} \ No newline at end of file diff --git a/src/main/java/com/interbank/challenge/config/jwt/JwtUtil.java b/src/main/java/com/interbank/challenge/config/jwt/JwtUtil.java new file mode 100644 index 0000000..bf97d0e --- /dev/null +++ b/src/main/java/com/interbank/challenge/config/jwt/JwtUtil.java @@ -0,0 +1,52 @@ +package com.interbank.challenge.config.jwt; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.Date; +import java.util.function.Function; + +@Component +public class JwtUtil { + + @Value("${app.jwt.secret}") + private String jwtSecret; + + @Value("${app.jwt.expirationMs}") + private int jwtExpirationMs; + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody(); + } + + private Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public String generateToken(String username) { + return Jwts.builder().setSubject(username).setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date((new Date()).getTime() + jwtExpirationMs)) + .signWith(SignatureAlgorithm.HS256, jwtSecret).compact(); + } + + public Boolean validateToken(String token, String username) { + final String extractedUsername = extractUsername(token); + return (extractedUsername.equals(username) && !isTokenExpired(token)); + } +} \ No newline at end of file diff --git a/src/main/java/com/interbank/challenge/config/kafka/KafkaConsumer.java b/src/main/java/com/interbank/challenge/config/kafka/KafkaConsumer.java new file mode 100644 index 0000000..2c372da --- /dev/null +++ b/src/main/java/com/interbank/challenge/config/kafka/KafkaConsumer.java @@ -0,0 +1,37 @@ +package com.interbank.challenge.config.kafka; + +import com.interbank.challenge.util.Constante; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; + +import java.util.HashMap; +import java.util.Map; + +@EnableKafka +@Configuration +public class KafkaConsumer { + + @Bean + public ConsumerFactory consumerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, Constante.SERVER_KAFKA ); + configProps.put(ConsumerConfig.GROUP_ID_CONFIG, Constante.MY_GROUP); + configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + return new DefaultKafkaConsumerFactory<>(configProps); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + return factory; + } + +} diff --git a/src/main/java/com/interbank/challenge/config/kafka/KafkaProducer.java b/src/main/java/com/interbank/challenge/config/kafka/KafkaProducer.java new file mode 100644 index 0000000..258ed05 --- /dev/null +++ b/src/main/java/com/interbank/challenge/config/kafka/KafkaProducer.java @@ -0,0 +1,29 @@ +package com.interbank.challenge.config.kafka; + +import com.interbank.challenge.util.Constante; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; + +import java.util.HashMap; +import java.util.Map; + +public class KafkaProducer { + + @Bean + public ProducerFactory producerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, Constante.SERVER_KAFKA); + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } +} diff --git a/src/main/java/com/interbank/challenge/controller/LoginController.java b/src/main/java/com/interbank/challenge/controller/LoginController.java new file mode 100644 index 0000000..ce6ab44 --- /dev/null +++ b/src/main/java/com/interbank/challenge/controller/LoginController.java @@ -0,0 +1,86 @@ +package com.interbank.challenge.controller; + +import com.interbank.challenge.config.jwt.JwtUtil; +import com.interbank.challenge.entity.UsuarioDocument; +import com.interbank.challenge.entity.dto.request.SigninRequest; +import com.interbank.challenge.entity.dto.response.MessageResponse; +import com.interbank.challenge.entity.dto.response.SigninResponse; +import com.interbank.challenge.service.UsuarioService; +import com.interbank.challenge.util.Constante; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +@RestController +@RequestMapping("/api/login") +public class LoginController { + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private ReactiveUserDetailsService CustomUserDetailsService; + + @Autowired + private UsuarioService usuarioService; + + @PostMapping("/signin") + @Operation(summary = "Metodo signin", description = "Devuelve el token en jwt") + public Mono> createAuthenticationToken(@Valid @RequestBody SigninRequest signinRequest) { + + return usuarioService.findByUsername(signinRequest.getUsername()) + .flatMap(userDetails -> { + if (userDetails != null && usuarioService.comparePasswords(signinRequest.getPassword(), userDetails.getPassword())) { + String jwt = jwtUtil.generateToken(signinRequest.getUsername()); + return Mono.just(ResponseEntity.ok(SigninResponse.builder() + .accessToken(jwt) + .tokenType(Constante.BEARER) + .id(userDetails.getId()) + .username(signinRequest.getUsername()) + .email(userDetails.getEmail()) + .locked(userDetails.getLocked()) + .disable(userDetails.getDisable()) + .role(userDetails.getRoleDocument().getRole()) + .build())); + } else { + return Mono.just(ResponseEntity.status(401).body(MessageResponse.builder() + .codeResponse(401) + .messageResponse(Constante.INVALID_CREDENTIAL) + .build())); + } + }); + } + + @PostMapping("/signup") + @Operation(summary = "Metodo signup", description = "Registra un Usuario en la base de datos") + public Mono> saveUser(@RequestBody UsuarioDocument user) { + + return usuarioService.findByUsername(user.getUsername()) + .flatMap(existingUser -> { + return Mono.just(ResponseEntity.status(HttpStatus.CONFLICT).body(MessageResponse.builder() + .codeResponse(HttpStatus.CONFLICT.value()) + .messageResponse("User already exists") + .build())); + }) + .switchIfEmpty( + usuarioService.saveUser(user) + .map(savedUser -> ResponseEntity.status(HttpStatus.CREATED).body(MessageResponse.builder() + .codeResponse(HttpStatus.CREATED.value()) + .messageResponse("Successful registration") + .build())) + .onErrorResume(e -> { + System.err.println("Error saving user: " + e.getMessage()); + return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(MessageResponse.builder() + .codeResponse(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .messageResponse("Service error") + .build())); + }) + ); + } + +} \ No newline at end of file diff --git a/src/main/java/com/interbank/challenge/controller/TransactionController.java b/src/main/java/com/interbank/challenge/controller/TransactionController.java new file mode 100644 index 0000000..66e7c34 --- /dev/null +++ b/src/main/java/com/interbank/challenge/controller/TransactionController.java @@ -0,0 +1,41 @@ +package com.interbank.challenge.controller; + +import com.interbank.challenge.entity.dto.request.TransactionRequest; +import com.interbank.challenge.entity.dto.response.TransactionResponse; +import com.interbank.challenge.service.TransactionService; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/transactions") +public class TransactionController { + + @Autowired + TransactionService transactionService; + + @PostMapping("/save") + @Operation(summary = "Metodo createTransaction", description = "Crea una operación") + public Mono>> createTransaction(@RequestHeader("Authorization") String token, @Valid @RequestBody TransactionRequest transaction) { + return transactionService.createTransaction(transaction) + .map(transactionId -> { + Map response = new HashMap<>(); + response.put("idTransaction", transactionId.getId()); + response.put("message", "El registro se hizo correctamente"); + return ResponseEntity.ok(response); + }); + + } + + @GetMapping("/list/{id}") + @Operation(summary = "Metodo getTransaction", description = "Devuelve una operacion") + public Mono getTransaction(@RequestHeader("Authorization") String token, @Valid @PathVariable String id) { + return transactionService.getTransactionById(id); + } +} diff --git a/src/main/java/com/interbank/challenge/entity/RoleDocument.java b/src/main/java/com/interbank/challenge/entity/RoleDocument.java new file mode 100644 index 0000000..06a025f --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/RoleDocument.java @@ -0,0 +1,28 @@ +package com.interbank.challenge.entity; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +@Document(collection = "table_role") +@Data +@NoArgsConstructor +public class RoleDocument { + + public RoleDocument(String role, LocalDate date){ + this.role=role; + this.date=date; + } + @Id + private String id; + + private String role; + + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate date; + +} diff --git a/src/main/java/com/interbank/challenge/entity/TransactionDocument.java b/src/main/java/com/interbank/challenge/entity/TransactionDocument.java new file mode 100644 index 0000000..c53ee68 --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/TransactionDocument.java @@ -0,0 +1,42 @@ +package com.interbank.challenge.entity; + + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +@Document(collection = "table_transaction") +@Data +@NoArgsConstructor +public class TransactionDocument { + + public TransactionDocument(String accountExternalIdDebit, String accountExternalIdCredit, Integer tranferTypeId, Integer value, String transactionStatus, LocalDate createdAt) { + this.accountExternalIdDebit = accountExternalIdDebit; + this.accountExternalIdCredit = accountExternalIdCredit; + this.tranferTypeId = tranferTypeId; + this.value = value; + this.transactionStatus = transactionStatus; + this.createdAt = createdAt; + } + + @Id + private String id; + + private String accountExternalIdDebit; + + private String accountExternalIdCredit; + + private Integer tranferTypeId; + + private Integer value; + + private String transactionStatus; + + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate createdAt; + +} diff --git a/src/main/java/com/interbank/challenge/entity/UsuarioDocument.java b/src/main/java/com/interbank/challenge/entity/UsuarioDocument.java new file mode 100644 index 0000000..29e9523 --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/UsuarioDocument.java @@ -0,0 +1,49 @@ +package com.interbank.challenge.entity; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; + +@Document(collection = "table_user") +@Data +@NoArgsConstructor +public class UsuarioDocument { + + public UsuarioDocument(String username, String password, String email, Boolean locked, Boolean disable) { + this.username = username; + this.password = password; + this.email = email; + this.locked = locked; + this.disable = disable; + } + + public UsuarioDocument(String username, String password, String email, Boolean locked, Boolean disable, RoleDocument roleDocument) { + this.username = username; + this.password = password; + this.email = email; + this.locked = locked; + this.disable = disable; + this.roleDocument = roleDocument; + } + + @Id + private String id; + + private String username; + + private String password; + + private String email; + + private Boolean locked; + + private Boolean disable; + + private RoleDocument roleDocument; + + +} + + + diff --git a/src/main/java/com/interbank/challenge/entity/dto/request/SigninRequest.java b/src/main/java/com/interbank/challenge/entity/dto/request/SigninRequest.java new file mode 100644 index 0000000..f3175d4 --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/dto/request/SigninRequest.java @@ -0,0 +1,14 @@ +package com.interbank.challenge.entity.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import lombok.Data; + +@Data +public class SigninRequest { + + @NotEmpty(message = "Username cannot be empty") + private String username; + + @NotEmpty(message = "Password cannot be empty") + private String password; +} diff --git a/src/main/java/com/interbank/challenge/entity/dto/request/SignupRequest.java b/src/main/java/com/interbank/challenge/entity/dto/request/SignupRequest.java new file mode 100644 index 0000000..d977bff --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/dto/request/SignupRequest.java @@ -0,0 +1,15 @@ +package com.interbank.challenge.entity.dto.request; + +import lombok.Data; + +@Data +public class SignupRequest { + + private String user; + private String password; + private String email; + private boolean lock; + private boolean disable; + private String role; + +} diff --git a/src/main/java/com/interbank/challenge/entity/dto/request/TransactionRequest.java b/src/main/java/com/interbank/challenge/entity/dto/request/TransactionRequest.java new file mode 100644 index 0000000..af3c70b --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/dto/request/TransactionRequest.java @@ -0,0 +1,38 @@ +package com.interbank.challenge.entity.dto.request; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import jakarta.validation.constraints.Size; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; + +@Data +public class TransactionRequest { + + @Size(min = 36, max = 36) + private String id; + + @NotNull + @Size(min = 36, max = 36) + private String accountDebit; + + @NotNull + @Size(min = 36, max = 36) + private String accountCredit; + + @NotNull + @Positive + private Integer type; + + @NotNull + @Positive + private Integer values; + + private String status; + + @DateTimeFormat(pattern = "yyyy-MM-dd") + private LocalDate date; + +} diff --git a/src/main/java/com/interbank/challenge/entity/dto/response/MessageResponse.java b/src/main/java/com/interbank/challenge/entity/dto/response/MessageResponse.java new file mode 100644 index 0000000..3b0ea7f --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/dto/response/MessageResponse.java @@ -0,0 +1,12 @@ +package com.interbank.challenge.entity.dto.response; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class MessageResponse { + + private Integer codeResponse; + private String messageResponse; +} diff --git a/src/main/java/com/interbank/challenge/entity/dto/response/SigninResponse.java b/src/main/java/com/interbank/challenge/entity/dto/response/SigninResponse.java new file mode 100644 index 0000000..2851ade --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/dto/response/SigninResponse.java @@ -0,0 +1,20 @@ +package com.interbank.challenge.entity.dto.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; + +@Data +@AllArgsConstructor +@Builder +public class SigninResponse { + + private String accessToken; + private String tokenType = "Bearer"; + private String id; + private String username; + private String email; + private Boolean locked = true; + private Boolean disable = true; + private String role; +} diff --git a/src/main/java/com/interbank/challenge/entity/dto/response/SignupResponse.java b/src/main/java/com/interbank/challenge/entity/dto/response/SignupResponse.java new file mode 100644 index 0000000..80ba6c4 --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/dto/response/SignupResponse.java @@ -0,0 +1,16 @@ +package com.interbank.challenge.entity.dto.response; + +import lombok.Data; + +@Data +public class SignupResponse { + + private String accessToken; + private String tokenType = "Bearer"; + private String id; + private String username; + private String email; + private Boolean locked = true; + private Boolean disable = true; + private String role; +} diff --git a/src/main/java/com/interbank/challenge/entity/dto/response/TransactionResponse.java b/src/main/java/com/interbank/challenge/entity/dto/response/TransactionResponse.java new file mode 100644 index 0000000..3a40f30 --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/dto/response/TransactionResponse.java @@ -0,0 +1,13 @@ +package com.interbank.challenge.entity.dto.response; + +import lombok.Data; + +@Data +public class TransactionResponse { + private String transactionId; + private TransactionType type; + private TypeStatus status; + private Integer value; + private String date; + +} diff --git a/src/main/java/com/interbank/challenge/entity/dto/response/TransactionType.java b/src/main/java/com/interbank/challenge/entity/dto/response/TransactionType.java new file mode 100644 index 0000000..490023a --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/dto/response/TransactionType.java @@ -0,0 +1,8 @@ +package com.interbank.challenge.entity.dto.response; + +import lombok.Data; + +@Data +public class TransactionType { + private String name; +} diff --git a/src/main/java/com/interbank/challenge/entity/dto/response/TypeStatus.java b/src/main/java/com/interbank/challenge/entity/dto/response/TypeStatus.java new file mode 100644 index 0000000..91fff31 --- /dev/null +++ b/src/main/java/com/interbank/challenge/entity/dto/response/TypeStatus.java @@ -0,0 +1,8 @@ +package com.interbank.challenge.entity.dto.response; + +import lombok.Data; + +@Data +public class TypeStatus { + private String name; +} diff --git a/src/main/java/com/interbank/challenge/mapper/TransactionMapper.java b/src/main/java/com/interbank/challenge/mapper/TransactionMapper.java new file mode 100644 index 0000000..1808539 --- /dev/null +++ b/src/main/java/com/interbank/challenge/mapper/TransactionMapper.java @@ -0,0 +1,29 @@ +package com.interbank.challenge.mapper; + +import com.interbank.challenge.entity.TransactionDocument; +import com.interbank.challenge.entity.dto.request.TransactionRequest; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface TransactionMapper { + + TransactionMapper INSTANCE = Mappers.getMapper(TransactionMapper.class); + + @Mapping(source = "accountDebit", target = "accountExternalIdDebit") + @Mapping(source = "accountCredit", target = "accountExternalIdCredit") + @Mapping(source = "type", target = "tranferTypeId") + @Mapping(source = "values", target = "value") + @Mapping(source = "status", target = "transactionStatus") + @Mapping(source = "date", target = "createdAt") + TransactionDocument toTransactionDocument(TransactionRequest request); + + @Mapping(source = "accountExternalIdDebit", target = "accountDebit") + @Mapping(source = "accountExternalIdCredit", target = "accountCredit") + @Mapping(source = "tranferTypeId", target = "type") + @Mapping(source = "value", target = "values") + @Mapping(source = "transactionStatus", target = "status") + @Mapping(source = "createdAt", target = "date") + TransactionRequest toTransactionRequest(TransactionDocument document); +} diff --git a/src/main/java/com/interbank/challenge/reporitory/RoleRepository.java b/src/main/java/com/interbank/challenge/reporitory/RoleRepository.java new file mode 100644 index 0000000..e903ea5 --- /dev/null +++ b/src/main/java/com/interbank/challenge/reporitory/RoleRepository.java @@ -0,0 +1,10 @@ +package com.interbank.challenge.reporitory; + +import com.interbank.challenge.entity.RoleDocument; +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface RoleRepository extends ReactiveMongoRepository { + +} diff --git a/src/main/java/com/interbank/challenge/reporitory/TransactionRepository.java b/src/main/java/com/interbank/challenge/reporitory/TransactionRepository.java new file mode 100644 index 0000000..3dc7f1a --- /dev/null +++ b/src/main/java/com/interbank/challenge/reporitory/TransactionRepository.java @@ -0,0 +1,11 @@ +package com.interbank.challenge.reporitory; + +import com.interbank.challenge.entity.TransactionDocument; +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface TransactionRepository extends ReactiveMongoRepository { + +} + diff --git a/src/main/java/com/interbank/challenge/reporitory/UsuarioRepository.java b/src/main/java/com/interbank/challenge/reporitory/UsuarioRepository.java new file mode 100644 index 0000000..6591990 --- /dev/null +++ b/src/main/java/com/interbank/challenge/reporitory/UsuarioRepository.java @@ -0,0 +1,13 @@ +package com.interbank.challenge.reporitory; + +import com.interbank.challenge.entity.UsuarioDocument; +import org.springframework.data.mongodb.repository.ReactiveMongoRepository; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Mono; + +@Repository +public interface UsuarioRepository extends ReactiveMongoRepository { + + Mono findByUsername(String username); + +} diff --git a/src/main/java/com/interbank/challenge/service/AntifraudeService.java b/src/main/java/com/interbank/challenge/service/AntifraudeService.java new file mode 100644 index 0000000..684bf05 --- /dev/null +++ b/src/main/java/com/interbank/challenge/service/AntifraudeService.java @@ -0,0 +1,8 @@ +package com.interbank.challenge.service; + +import org.apache.kafka.clients.consumer.ConsumerRecord; + +public interface AntifraudeService { + + void listen(ConsumerRecord record); +} diff --git a/src/main/java/com/interbank/challenge/service/AntifraudeServiceImpl.java b/src/main/java/com/interbank/challenge/service/AntifraudeServiceImpl.java new file mode 100644 index 0000000..a66dda0 --- /dev/null +++ b/src/main/java/com/interbank/challenge/service/AntifraudeServiceImpl.java @@ -0,0 +1,46 @@ +package com.interbank.challenge.service; + +import com.interbank.challenge.entity.TransactionDocument; +import com.interbank.challenge.reporitory.TransactionRepository; +import com.interbank.challenge.util.Constante; +import com.interbank.challenge.util.TransactionStatus; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@Service +public class AntifraudeServiceImpl implements AntifraudeService { + + @Autowired + private KafkaTemplate kafkaTemplate; + + @Autowired + private TransactionRepository transactionRepository; + + private static final String TRANSACTION_CREATED_TOPIC = "transaction.created"; + private static final String TRANSACTION_STATUS_UPDATED_TOPIC = "transaction.status.updated"; + + @Override + @KafkaListener(topics = TRANSACTION_CREATED_TOPIC) + public void listen(ConsumerRecord record) { + String transactionId = record.value(); + Mono transactionMono = transactionRepository.findById(transactionId); + + transactionMono.subscribe(transaction -> { + if (transaction != null) { + if (transaction.getValue() > 1000) { + kafkaTemplate.send(TRANSACTION_STATUS_UPDATED_TOPIC, transactionId, TransactionStatus.RECHAZADO.toString()); + } else { + kafkaTemplate.send(TRANSACTION_STATUS_UPDATED_TOPIC, transactionId, TransactionStatus.APROBADO.toString()); + } + } else { + kafkaTemplate.send(TRANSACTION_STATUS_UPDATED_TOPIC, transactionId, Constante.TRANSACTION_FOUNT); + } + }, error -> { + kafkaTemplate.send(TRANSACTION_STATUS_UPDATED_TOPIC, transactionId, Constante.TRANSACTION_ERROR); + }); + } +} diff --git a/src/main/java/com/interbank/challenge/service/TransactionService.java b/src/main/java/com/interbank/challenge/service/TransactionService.java new file mode 100644 index 0000000..8ebf18a --- /dev/null +++ b/src/main/java/com/interbank/challenge/service/TransactionService.java @@ -0,0 +1,15 @@ +package com.interbank.challenge.service; + +import com.interbank.challenge.entity.TransactionDocument; +import com.interbank.challenge.entity.dto.request.TransactionRequest; +import com.interbank.challenge.entity.dto.response.TransactionResponse; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import reactor.core.publisher.Mono; + +public interface TransactionService { + + Mono createTransaction(TransactionRequest transaction) ; + void updateTransactionStatus(ConsumerRecord record); + Mono getTransactionById(String transactionId); + +} diff --git a/src/main/java/com/interbank/challenge/service/TransactionServiceImpl.java b/src/main/java/com/interbank/challenge/service/TransactionServiceImpl.java new file mode 100644 index 0000000..daa0a26 --- /dev/null +++ b/src/main/java/com/interbank/challenge/service/TransactionServiceImpl.java @@ -0,0 +1,90 @@ +package com.interbank.challenge.service; + +import com.interbank.challenge.entity.TransactionDocument; +import com.interbank.challenge.entity.dto.request.TransactionRequest; +import com.interbank.challenge.entity.dto.response.TransactionResponse; +import com.interbank.challenge.entity.dto.response.TransactionType; +import com.interbank.challenge.entity.dto.response.TypeStatus; +import com.interbank.challenge.mapper.TransactionMapper; +import com.interbank.challenge.reporitory.TransactionRepository; +import com.interbank.challenge.util.TransactionStatus; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; +import java.time.LocalDate; + +@Service +public class TransactionServiceImpl implements TransactionService{ + + @Autowired + TransactionRepository transactionRepository; + + private static final String TRANSACTION_CREATED_TOPIC = "transaction.created"; + private static final String TRANSACTION_STATUS_UPDATED_TOPIC = "transaction.status.updated"; + private final TransactionMapper transactionMapper = TransactionMapper.INSTANCE; + + @Autowired + private KafkaTemplate kafkaTemplate; + + + @Override + public Mono createTransaction(TransactionRequest transactionRequest) { + TransactionDocument transactionDocument = transactionMapper.toTransactionDocument(transactionRequest); + transactionDocument.setTransactionStatus(TransactionStatus.PENDIENTE.toString()); + transactionDocument.setCreatedAt(LocalDate.now()); + return transactionRepository.save(transactionDocument) + .doOnSuccess(transaction -> kafkaTemplate.send(TRANSACTION_CREATED_TOPIC, transaction.getId())) + .map(transactionMapper::toTransactionRequest); + } + + @Override + @KafkaListener(topics = TRANSACTION_STATUS_UPDATED_TOPIC) + public void updateTransactionStatus(ConsumerRecord record) { + String transactionId = record.key(); + String estado = record.value(); + TransactionStatus transactionStatus = TransactionStatus.fromString(estado); + transactionRepository.findById(transactionId) + .flatMap(transaction -> { + transaction.setTransactionStatus(transactionStatus.toString()); + return transactionRepository.save(transaction); + }) + .doOnError(error -> System.err.println("Error al actualizar la transacción: " + error.getMessage())) + .doOnSuccess(transaction -> System.out.println("Transacción actualizada con éxito.")) + .subscribe(); + } + + @Override + public Mono getTransactionById(String transactionId) { + return transactionRepository.findById(transactionId) + .map(transaction -> { + TransactionResponse response = new TransactionResponse(); + response.setTransactionId(transaction.getId()); + response.setValue(transaction.getValue()); + response.setDate(transaction.getCreatedAt().toString()); + + TransactionType type = new TransactionType(); + type.setName(getTransactionTypeName(transaction.getTranferTypeId())); + response.setType(type); + + TransactionStatus transactionStatus = TransactionStatus.fromString(transaction.getTransactionStatus()); + + TypeStatus typeStatus = new TypeStatus(); + typeStatus.setName(transactionStatus.toString()); + response.setStatus(typeStatus); + + return response; + }) + .switchIfEmpty(Mono.error(new RuntimeException("Transacción no encontrada"))); + } + + private String getTransactionTypeName(Integer typeId) { + switch (typeId) { + case 1: return "debido"; + case 2: return "credito"; + default: return "Unknown"; + } + } +} diff --git a/src/main/java/com/interbank/challenge/service/UserDetailsServiceImpl.java b/src/main/java/com/interbank/challenge/service/UserDetailsServiceImpl.java new file mode 100644 index 0000000..929fcc6 --- /dev/null +++ b/src/main/java/com/interbank/challenge/service/UserDetailsServiceImpl.java @@ -0,0 +1,28 @@ +package com.interbank.challenge.service; +; +import com.interbank.challenge.reporitory.UsuarioRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@Service +public class UserDetailsServiceImpl implements ReactiveUserDetailsService { + + @Autowired + private UsuarioRepository usuarioRepository; + + @Override + public Mono findByUsername(String username) { + return usuarioRepository.findByUsername(username) + .switchIfEmpty(Mono.error(new UsernameNotFoundException("User not found"))) + .map(user -> org.springframework.security.core.userdetails.User + .withUsername(user.getUsername()) + .password(user.getPassword()) + //.roles(user.getRoleDocument().getRole()) validar usar roles o authorites + .authorities(user.getRoleDocument().getRole()) + .build()); + } +} diff --git a/src/main/java/com/interbank/challenge/service/UsuarioService.java b/src/main/java/com/interbank/challenge/service/UsuarioService.java new file mode 100644 index 0000000..00704ce --- /dev/null +++ b/src/main/java/com/interbank/challenge/service/UsuarioService.java @@ -0,0 +1,11 @@ +package com.interbank.challenge.service; +; +import com.interbank.challenge.entity.UsuarioDocument; +import reactor.core.publisher.Mono; + +public interface UsuarioService { + + Mono findByUsername(String username); + Mono saveUser(UsuarioDocument user); + boolean comparePasswords(String rawPassword, String encodedPassword); +} diff --git a/src/main/java/com/interbank/challenge/service/UsuarioServiceImpl.java b/src/main/java/com/interbank/challenge/service/UsuarioServiceImpl.java new file mode 100644 index 0000000..2ba1db0 --- /dev/null +++ b/src/main/java/com/interbank/challenge/service/UsuarioServiceImpl.java @@ -0,0 +1,40 @@ +package com.interbank.challenge.service; +import com.interbank.challenge.entity.UsuarioDocument; +import com.interbank.challenge.reporitory.RoleRepository; +import com.interbank.challenge.reporitory.UsuarioRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.time.LocalDate; + +@Service +public class UsuarioServiceImpl implements UsuarioService{ + + @Autowired + private UsuarioRepository usuarioRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private BCryptPasswordEncoder bCryptPasswordEncoder; + + @Override + public Mono findByUsername(String username) { + return usuarioRepository.findByUsername(username); + } + + @Override + public Mono saveUser(UsuarioDocument user) { + user.setPassword(bCryptPasswordEncoder.encode(user.getPassword())); + user.getRoleDocument().setDate(LocalDate.now()); + return usuarioRepository.save(user); + } + + @Override + public boolean comparePasswords(String rawPassword, String encodedPassword) { + return bCryptPasswordEncoder.matches(rawPassword, encodedPassword); + } +} diff --git a/src/main/java/com/interbank/challenge/util/Constante.java b/src/main/java/com/interbank/challenge/util/Constante.java new file mode 100644 index 0000000..a55a196 --- /dev/null +++ b/src/main/java/com/interbank/challenge/util/Constante.java @@ -0,0 +1,11 @@ +package com.interbank.challenge.util; + +public class Constante { + + public static final String INVALID_CREDENTIAL = "Invalid credentials"; + public static final String BEARER = "Bearer"; + public static final String SERVER_KAFKA = "localhost:9092"; + public static final String MY_GROUP = "myGroup"; + public static final String TRANSACTION_FOUNT = "not_found"; + public static final String TRANSACTION_ERROR= "error"; +} diff --git a/src/main/java/com/interbank/challenge/util/TransactionStatus.java b/src/main/java/com/interbank/challenge/util/TransactionStatus.java new file mode 100644 index 0000000..7c036ec --- /dev/null +++ b/src/main/java/com/interbank/challenge/util/TransactionStatus.java @@ -0,0 +1,32 @@ +package com.interbank.challenge.util; + + +public enum TransactionStatus { + PENDIENTE("pendiente"), + APROBADO("aprobado"), + RECHAZADO("rechazado"); + + private final String displayName; + + TransactionStatus(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + @Override + public String toString() { + return displayName; + } + + public static TransactionStatus fromString(String status) { + for (TransactionStatus ts : TransactionStatus.values()) { + if (ts.displayName.equalsIgnoreCase(status)) { + return ts; + } + } + throw new IllegalArgumentException("Unknown status: " + status); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..1e7b903 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,48 @@ +app: + jwt: + secret: $2y$10$dJMr85s.761OiPWe.m2nRem3GCWiTWQmuPGY4MpayC7z6DH6LC4v2 + expirationMs: 300000 #5min + +spring: + application: + name: challenge + data: + mongodb: + #uri: mongodb://localhost:27017/challenge_prod + uri: ${SPRING_DATA_MONGODB_URI} + security: + user: + name: admin + password: admin + #autoconfigure: + #exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration + kafka: + #bootstrap-servers: localhost:9092 + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS} + consumer: + group-id: myGroup + auto-offset-reset: earliest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.apache.kafka.common.serialization.StringSerializer +server: + #port: 8082 + port: 8083 + +logging: + level: + root: info + org: + springframework: + web: info + security: DEBUG + kafka: DEBUG + hibernate: error + +springdoc: + api-docs: + path: /v3/api-docs + swagger-ui: + path: /swagger-ui.html \ No newline at end of file diff --git a/src/main/resources/collection-postman/Interbank.postman_collection.json b/src/main/resources/collection-postman/Interbank.postman_collection.json new file mode 100644 index 0000000..0ebc456 --- /dev/null +++ b/src/main/resources/collection-postman/Interbank.postman_collection.json @@ -0,0 +1,200 @@ +{ + "info": { + "_postman_id": "c6f08e19-66f6-43fe-b62e-dfd6f98a76dc", + "name": "Interbank", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "9325910", + "_collection_link": "https://lively-moon-301039.postman.co/workspace/PROY-PROYECTOS~f3143bb4-021d-48c7-8824-9d431947399d/collection/9325910-c6f08e19-66f6-43fe-b62e-dfd6f98a76dc?action=share&source=collection_link&creator=9325910" + }, + "item": [ + { + "name": "api-challenge-ms", + "item": [ + { + "name": "2. signin - obtener token", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"username\": \"prueba\",\r\n \"password\": \"prueba123\"\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8083/api/login/signin", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8083", + "path": [ + "api", + "login", + "signin" + ] + } + }, + "response": [] + }, + { + "name": "1. signup - crear usuario", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"username\" : \"prueba\",\r\n \"password\" : \"prueba123\",\r\n \"email\" : \"preuba@hotmail.com\",\r\n \"locked\" : true,\r\n \"disable\" : true,\r\n \"roleDocument\" : {\r\n \"id\" : \"663e6f02c7e1ec15a4d93e67\",\r\n \"role\" : \"ADMIN\"\r\n }\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8083/api/login/signup", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8083", + "path": [ + "api", + "login", + "signup" + ] + } + }, + "response": [] + }, + { + "name": "3. transactions/save - rechazado", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwcnVlYmEiLCJpYXQiOjE3MjMwNTYyNDQsImV4cCI6MTcyMzA1NjU0NH0.sf5w_w8yjmt2YuldFL4PVPrj6FzTYlpy9cdWlZwFfzQ", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"accountDebit\": \"136e4567-e89b-12d3-a456-426614174036\",\r\n \"accountCredit\": \"136e4567-e89b-12d3-a456-426614174036\",\r\n \"type\": 2,\r\n \"values\": 1200\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8083/api/transactions/save", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8083", + "path": [ + "api", + "transactions", + "save" + ] + } + }, + "response": [] + }, + { + "name": "4. transactions/save - aprobado", + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwcnVlYmEiLCJpYXQiOjE3MjMwNTYyNDQsImV4cCI6MTcyMzA1NjU0NH0.sf5w_w8yjmt2YuldFL4PVPrj6FzTYlpy9cdWlZwFfzQ", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\r\n \"accountDebit\": \"136e4567-e89b-12d3-a456-426614174036\",\r\n \"accountCredit\": \"136e4567-e89b-12d3-a456-426614174036\",\r\n \"type\": 2,\r\n \"values\": 900\r\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8083/api/transactions/save", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8083", + "path": [ + "api", + "transactions", + "save" + ] + } + }, + "response": [] + }, + { + "name": "5. Listar Transactions", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "bearer", + "bearer": [ + { + "key": "token", + "value": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwcnVlYmEiLCJpYXQiOjE3MjMwNTYyNDQsImV4cCI6MTcyMzA1NjU0NH0.sf5w_w8yjmt2YuldFL4PVPrj6FzTYlpy9cdWlZwFfzQ", + "type": "string" + } + ] + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8083/api/transactions/list/66b3c0a5b806f918e276d782", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8083", + "path": [ + "api", + "transactions", + "list", + "66b3c0a5b806f918e276d782" + ] + } + }, + "response": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/src/main/resources/documentacion/api-docs.yaml b/src/main/resources/documentacion/api-docs.yaml new file mode 100644 index 0000000..c852fba --- /dev/null +++ b/src/main/resources/documentacion/api-docs.yaml @@ -0,0 +1,204 @@ +openapi: 3.0.1 +info: + title: API OPERACIONES-MS + description: Servicio que registra operaciones transactionales manejando eventos en diferentes tópicos creados. Correctamente aunthenticado y autorizado para ejecución de cada recurso. + version: v0 +servers: +- url: http://localhost:8082 + description: Generated server url +paths: + /api/transactions/save: + post: + tags: + - transaction-controller + summary: Metodo createTransaction + description: Crea una operación + operationId: createTransaction + parameters: + - name: Authorization + in: header + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/TransactionRequest" + required: true + responses: + "200": + description: OK + content: + '*/*': + schema: + type: object + additionalProperties: + type: string + /api/login/signup: + post: + tags: + - login-controller + summary: Metodo signup + description: Registra un Usuario en la base de datos + operationId: saveUser + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/UsuarioDocument" + required: true + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/MessageResponse" + /api/login/signin: + post: + tags: + - login-controller + summary: Metodo signin + description: Devuelve el token en jwt + operationId: createAuthenticationToken + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/SigninRequest" + required: true + responses: + "200": + description: OK + content: + '*/*': + schema: + type: object + /api/transactions/list/{id}: + get: + tags: + - transaction-controller + summary: Metodo getTransaction + description: Devuelve una operacion + operationId: getTransaction + parameters: + - name: Authorization + in: header + required: true + schema: + type: string + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: OK + content: + '*/*': + schema: + $ref: "#/components/schemas/TransactionResponse" +components: + schemas: + TransactionRequest: + required: + - accountCredit + - accountDebit + - type + - values + type: object + properties: + id: + maxLength: 36 + minLength: 36 + type: string + accountDebit: + maxLength: 36 + minLength: 36 + type: string + accountCredit: + maxLength: 36 + minLength: 36 + type: string + type: + type: integer + format: int32 + values: + type: integer + format: int32 + status: + type: string + date: + type: string + format: date + RoleDocument: + type: object + properties: + id: + type: string + role: + type: string + date: + type: string + format: date + UsuarioDocument: + type: object + properties: + id: + type: string + username: + type: string + password: + type: string + email: + type: string + locked: + type: boolean + disable: + type: boolean + roleDocument: + $ref: "#/components/schemas/RoleDocument" + MessageResponse: + type: object + properties: + codeResponse: + type: integer + format: int32 + messageResponse: + type: string + SigninRequest: + required: + - password + - username + type: object + properties: + username: + type: string + password: + type: string + TransactionResponse: + type: object + properties: + transactionId: + type: string + type: + $ref: "#/components/schemas/TransactionType" + status: + $ref: "#/components/schemas/TypeStatus" + value: + type: integer + format: int32 + date: + type: string + TransactionType: + type: object + properties: + name: + type: string + TypeStatus: + type: object + properties: + name: + type: string diff --git a/src/main/resources/mongo_db/challenge_prod.table_role.json b/src/main/resources/mongo_db/challenge_prod.table_role.json new file mode 100644 index 0000000..fc8159c --- /dev/null +++ b/src/main/resources/mongo_db/challenge_prod.table_role.json @@ -0,0 +1,30 @@ +[{ + "_id": { + "$oid": "663e6f02c7e1ec15a4d93e66" + }, + "role": "USER", + "date": { + "$date": "2024-08-02T05:00:00.000Z" + }, + "_class": "com.interbank.challenge.entity.RoleDocum" +}, +{ + "_id": { + "$oid": "663e6f02c7e1ec15a4d93e67" + }, + "role": "ADMIN", + "date": { + "$date": "2024-08-02T05:00:00.000Z" + }, + "_class": "com.interbank.challenge.entity.RoleDocument" +}, +{ + "_id": { + "$oid": "663e6f02c7e1ec15a4d93e68" + }, + "role": "CUSTOMER", + "date": { + "$date": "2024-08-02T05:00:00.000Z" + }, + "_class": "com.estrasys.authentication.entity.RoleDocument" +}] \ No newline at end of file diff --git a/src/main/resources/mongo_db/challenge_prod.table_transaction.json b/src/main/resources/mongo_db/challenge_prod.table_transaction.json new file mode 100644 index 0000000..aa1d792 --- /dev/null +++ b/src/main/resources/mongo_db/challenge_prod.table_transaction.json @@ -0,0 +1,28 @@ +[{ + "_id": { + "$oid": "66b304e8d0879a0d773e7fe3" + }, + "accountExternalIdDebit": "136e4567-e89b-12d3-a456-426614174036", + "accountExternalIdCredit": "136e4567-e89b-12d3-a456-426614174036", + "tranferTypeId": 2, + "value": 1200, + "transactionStatus": "rechazado", + "createdAt": { + "$date": "2024-08-07T05:00:00.000Z" + }, + "_class": "com.interbank.challenge.entity.TransactionDocument" +}, +{ + "_id": { + "$oid": "66b304f8d0879a0d773e7fe4" + }, + "accountExternalIdDebit": "136e4567-e89b-12d3-a456-426614174036", + "accountExternalIdCredit": "136e4567-e89b-12d3-a456-426614174036", + "tranferTypeId": 2, + "value": 900, + "transactionStatus": "aprobado", + "createdAt": { + "$date": "2024-08-07T05:00:00.000Z" + }, + "_class": "com.interbank.challenge.entity.TransactionDocument" +}] \ No newline at end of file diff --git a/src/main/resources/mongo_db/challenge_prod.table_user.json b/src/main/resources/mongo_db/challenge_prod.table_user.json new file mode 100644 index 0000000..61a7200 --- /dev/null +++ b/src/main/resources/mongo_db/challenge_prod.table_user.json @@ -0,0 +1,20 @@ +[{ + "_id": { + "$oid": "66ad7a97a28fbe088fa677f2" + }, + "username": "prueba", + "password": "$2a$10$GROKwgPpk3Bm0pkQs7zlM.1vK9JcP0HKCeunsUyYWYbIbea36Zdmm", + "email": "prueba@hotmail.com", + "locked": true, + "disable": true, + "roleDocument": { + "_id": { + "$oid": "663e6f02c7e1ec15a4d93e67" + }, + "role": "ADMIN", + "date": { + "$date": "2024-08-02T05:00:00.000Z" + } + }, + "_class": "com.interbank.challenge.entity.UsuarioDocument" +}] \ No newline at end of file diff --git a/src/test/java/com/interbank/challenge/ChallengeApplicationTests.java b/src/test/java/com/interbank/challenge/ChallengeApplicationTests.java new file mode 100644 index 0000000..1352882 --- /dev/null +++ b/src/test/java/com/interbank/challenge/ChallengeApplicationTests.java @@ -0,0 +1,13 @@ +package com.interbank.challenge; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ChallengeApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/interbank/challenge/controller/LoginControllerTest.java b/src/test/java/com/interbank/challenge/controller/LoginControllerTest.java new file mode 100644 index 0000000..24f9bc1 --- /dev/null +++ b/src/test/java/com/interbank/challenge/controller/LoginControllerTest.java @@ -0,0 +1,98 @@ +package com.interbank.challenge.controller; + +import com.interbank.challenge.config.jwt.JwtUtil; +import com.interbank.challenge.entity.RoleDocument; +import com.interbank.challenge.entity.UsuarioDocument; +import com.interbank.challenge.entity.dto.request.SigninRequest; +import com.interbank.challenge.entity.dto.response.MessageResponse; +import com.interbank.challenge.entity.dto.response.SigninResponse; +import com.interbank.challenge.service.UsuarioService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.ResponseEntity; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +class LoginControllerTest { + + @InjectMocks + private LoginController loginController; + + @Mock + private JwtUtil jwtUtil; + + @Mock + private UsuarioService usuarioService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("crear un token - debe devolver token") + void createAuthenticationToken_ShouldReturnToken() { + SigninRequest signinRequest = new SigninRequest(); + signinRequest.setUsername("prueba"); + signinRequest.setPassword("prueba123"); + + UsuarioDocument user = new UsuarioDocument(); + user.setId("testId"); + user.setUsername(signinRequest.getUsername()); + user.setPassword("$2a$10$7d1E6Pe0Uv5A2l7OQ2iF/OuK4a1yLXwh56o5jGTHkA5Vijyz/ZGSO"); + user.setEmail("prueba@example.com"); + user.setLocked(false); + user.setDisable(false); + + RoleDocument role = new RoleDocument(); + role.setRole("USER_ADMIN"); + user.setRoleDocument(role); + + when(usuarioService.findByUsername(signinRequest.getUsername())).thenReturn(Mono.just(user)); + when(usuarioService.comparePasswords(signinRequest.getPassword(), user.getPassword())).thenReturn(true); + when(jwtUtil.generateToken(signinRequest.getUsername())).thenReturn("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwcnVlYmEiLCJpYXQiOjE3MjMwMDQ0OTEsImV4cCI6MTcyMzAwNDc5MX0.XKTk7TJm1WOtrZSjmDsHR3XmwEIk_Qf8AK4FtlTyXRI"); + + ResponseEntity responseEntity = loginController.createAuthenticationToken(signinRequest).block(); + + assertNotNull(responseEntity); + assertInstanceOf(SigninResponse.class, responseEntity.getBody()); + + SigninResponse signinResponse = (SigninResponse) responseEntity.getBody(); + assertEquals("eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJwcnVlYmEiLCJpYXQiOjE3MjMwMDQ0OTEsImV4cCI6MTcyMzAwNDc5MX0.XKTk7TJm1WOtrZSjmDsHR3XmwEIk_Qf8AK4FtlTyXRI", signinResponse.getAccessToken()); + assertEquals("Bearer", signinResponse.getTokenType()); + assertEquals("testId", signinResponse.getId()); + assertEquals("prueba", signinResponse.getUsername()); + assertEquals("prueba@example.com", signinResponse.getEmail()); + assertFalse(signinResponse.getLocked()); + assertFalse(signinResponse.getDisable()); + assertEquals("USER_ADMIN", signinResponse.getRole()); + } + + @Test + @DisplayName("Guarda usuario - devuelve mensaje regsitro correcto") + void saveUser_ShouldReturnSuccessMessage() { + UsuarioDocument user = new UsuarioDocument(); + + user.setUsername("testUser"); + user.setPassword("testPassword"); + + when(usuarioService.findByUsername(anyString())).thenReturn(Mono.empty()); + when(usuarioService.saveUser(any(UsuarioDocument.class))).thenReturn(Mono.just(user)); + + ResponseEntity responseEntity = loginController.saveUser(user).block(); + + assertNotNull(responseEntity); + assertInstanceOf(MessageResponse.class, responseEntity.getBody()); + + MessageResponse messageResponse = (MessageResponse) responseEntity.getBody(); + assertEquals("Successful registration", messageResponse.getMessageResponse()); + } +} diff --git a/src/test/java/com/interbank/challenge/controller/TransactionControllerTest.java b/src/test/java/com/interbank/challenge/controller/TransactionControllerTest.java new file mode 100644 index 0000000..b156beb --- /dev/null +++ b/src/test/java/com/interbank/challenge/controller/TransactionControllerTest.java @@ -0,0 +1,62 @@ +package com.interbank.challenge.controller; + +import com.interbank.challenge.entity.dto.request.TransactionRequest; +import com.interbank.challenge.entity.dto.response.TransactionResponse; +import com.interbank.challenge.service.TransactionService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.ResponseEntity; +import reactor.core.publisher.Mono; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +class TransactionControllerTest { + + @InjectMocks + private TransactionController transactionController; + + @Mock + private TransactionService transactionService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("Crea una Transaccion - retorna mensage exitoso") + void createTransaction_ShouldReturnSuccessMessage() { + TransactionRequest transactionRequest = new TransactionRequest(); + + when(transactionService.createTransaction(any(TransactionRequest.class))) + .thenReturn(Mono.just(transactionRequest)); + + ResponseEntity> responseEntity = transactionController + .createTransaction("Bearer token", transactionRequest) + .block(); + + assertEquals("El registro se hizo correctamente", responseEntity.getBody().get("message")); + } + + @Test + @DisplayName("obtiene una transaccion") + void getTransaction_ShouldReturnTransactionResponse() { + String transactionId = "123"; + TransactionResponse response = new TransactionResponse(); + + when(transactionService.getTransactionById(transactionId)) + .thenReturn(Mono.just(response)); + + TransactionResponse result = transactionController.getTransaction("Bearer token", transactionId).block(); + + assertEquals(response, result); + } +} diff --git a/src/test/java/com/interbank/challenge/service/AntifraudeServiceImplTest.java b/src/test/java/com/interbank/challenge/service/AntifraudeServiceImplTest.java new file mode 100644 index 0000000..c36ffb2 --- /dev/null +++ b/src/test/java/com/interbank/challenge/service/AntifraudeServiceImplTest.java @@ -0,0 +1,70 @@ +package com.interbank.challenge.service; + +import com.interbank.challenge.entity.TransactionDocument; +import com.interbank.challenge.reporitory.TransactionRepository; +import com.interbank.challenge.util.TransactionStatus; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.kafka.core.KafkaTemplate; +import reactor.core.publisher.Mono; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +class AntifraudeServiceImplTest { + + @InjectMocks + private AntifraudeServiceImpl antifraudeService; + + @Mock + private KafkaTemplate kafkaTemplate; + + @Mock + private TransactionRepository transactionRepository; + + private static final String TRANSACTION_CREATED_TOPIC = "transaction.created"; + private static final String TRANSACTION_STATUS_UPDATED_TOPIC = "transaction.status.updated"; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("Transaccion aprobada cuando el valor es menor a mil") + void listen_ShouldApproveTransaction_WhenValueIsLessThanThreshold() { + String transactionId = "123"; + ConsumerRecord record = new ConsumerRecord<>(TRANSACTION_CREATED_TOPIC, 0, 0, null, transactionId); + + TransactionDocument transaction = new TransactionDocument(); + transaction.setValue(500); + when(transactionRepository.findById(transactionId)).thenReturn(Mono.just(transaction)); + + antifraudeService.listen(record); + + verify(kafkaTemplate, times(1)) + .send(eq(TRANSACTION_STATUS_UPDATED_TOPIC), eq(transactionId), eq(TransactionStatus.APROBADO.toString())); + } + + @Test + @DisplayName("Transaccion rechazada cuando el valor es mayor a mil") + void listen_ShouldRejectTransaction_WhenValueIsGreaterThanThreshold() { + String transactionId = "123"; + ConsumerRecord record = new ConsumerRecord<>(TRANSACTION_CREATED_TOPIC, 0, 0, null, transactionId); + + TransactionDocument transaction = new TransactionDocument(); + transaction.setValue(1500); + when(transactionRepository.findById(transactionId)).thenReturn(Mono.just(transaction)); + + antifraudeService.listen(record); + + verify(kafkaTemplate, times(1)) + .send(eq(TRANSACTION_STATUS_UPDATED_TOPIC), eq(transactionId), eq(TransactionStatus.RECHAZADO.toString())); + } + +} \ No newline at end of file diff --git a/src/test/java/com/interbank/challenge/service/TransactionServiceImpl.java b/src/test/java/com/interbank/challenge/service/TransactionServiceImpl.java new file mode 100644 index 0000000..2b97ad3 --- /dev/null +++ b/src/test/java/com/interbank/challenge/service/TransactionServiceImpl.java @@ -0,0 +1,79 @@ +package com.interbank.challenge.service; + +import com.interbank.challenge.entity.TransactionDocument; +import com.interbank.challenge.entity.dto.request.TransactionRequest; +import com.interbank.challenge.entity.dto.response.TransactionResponse; +import com.interbank.challenge.mapper.TransactionMapper; +import com.interbank.challenge.reporitory.TransactionRepository; +import com.interbank.challenge.util.TransactionStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.kafka.core.KafkaTemplate; +import reactor.core.publisher.Mono; +import java.time.LocalDate; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +class TransactionServiceImplTest { + + @InjectMocks + private TransactionServiceImpl transactionService; + + @Mock + private TransactionRepository transactionRepository; + + @Mock + private KafkaTemplate kafkaTemplate; + + private final TransactionMapper transactionMapper = TransactionMapper.INSTANCE; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("Crea una Transaccion - retorna mensage exitoso") + void createTransaction_ShouldReturnTransactionRequest() { + TransactionRequest request = new TransactionRequest(); + + TransactionDocument document = transactionMapper.toTransactionDocument(request); + when(transactionRepository.save(any(TransactionDocument.class))) + .thenReturn(Mono.just(document)); + + TransactionRequest result = transactionService.createTransaction(request).block(); + + assertEquals(request, result); + } + + @Test + @DisplayName("obtiene una transaccion por ID") + void getTransactionById_ShouldReturnTransactionResponse() { + + TransactionDocument transactionDocument = new TransactionDocument(); + transactionDocument.setId("testTransactionId"); + transactionDocument.setValue(1500); + transactionDocument.setTranferTypeId(1); + transactionDocument.setTransactionStatus(TransactionStatus.PENDIENTE.toString()); + transactionDocument.setCreatedAt(LocalDate.of(2024, 8, 6)); + + when(transactionRepository.findById("testTransactionId")).thenReturn(Mono.just(transactionDocument)); + + Mono transactionResponseMono = transactionService.getTransactionById("testTransactionId");; + + TransactionResponse transactionResponse = transactionResponseMono.block(); + assertNotNull(transactionResponse); + assertEquals("testTransactionId", transactionResponse.getTransactionId()); + assertEquals(1500, transactionResponse.getValue()); + assertEquals("2024-08-06", transactionResponse.getDate()); + assertEquals("debido", transactionResponse.getType().getName()); + assertEquals(TransactionStatus.PENDIENTE.toString(), transactionResponse.getStatus().getName()); + } + +} \ No newline at end of file diff --git a/src/test/java/com/interbank/challenge/service/UsuarioServiceImpl.java b/src/test/java/com/interbank/challenge/service/UsuarioServiceImpl.java new file mode 100644 index 0000000..45fb7a0 --- /dev/null +++ b/src/test/java/com/interbank/challenge/service/UsuarioServiceImpl.java @@ -0,0 +1,68 @@ +package com.interbank.challenge.service; + +import com.interbank.challenge.entity.RoleDocument; +import com.interbank.challenge.entity.UsuarioDocument; +import com.interbank.challenge.reporitory.UsuarioRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import reactor.core.publisher.Mono; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; + +class UsuarioServiceImplTest { + + @InjectMocks + private UsuarioServiceImpl usuarioService; + + @Mock + private UsuarioRepository usuarioRepository; + + @Mock + private BCryptPasswordEncoder bCryptPasswordEncoder; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("Guarda Usuario - regsitro exitoso") + void saveUser_ShouldReturnSavedUser() { + UsuarioDocument user = new UsuarioDocument(); + user.setUsername("prueba"); + user.setPassword("prueba123"); + + RoleDocument roleDocument = new RoleDocument(); + roleDocument.setRole("ROLE_ADMIN"); + user.setRoleDocument(roleDocument); + + String encodedPassword = "encodedPassword"; + + when(bCryptPasswordEncoder.encode("prueba123")).thenReturn(encodedPassword); + when(usuarioRepository.save(any(UsuarioDocument.class))).thenReturn(Mono.just(user)); + Mono savedUserMono = usuarioService.saveUser(user); + + UsuarioDocument savedUser = savedUserMono.block(); + assertEquals("prueba", savedUser.getUsername()); + assertEquals(encodedPassword, savedUser.getPassword()); + assertNotNull(savedUser.getRoleDocument()); + assertEquals("ROLE_ADMIN", savedUser.getRoleDocument().getRole()); + } + + @Test + @DisplayName("Validacion de password") + void comparePasswords_ShouldReturnTrue() { + when(bCryptPasswordEncoder.matches(any(), any())).thenReturn(true); + + boolean result = usuarioService.comparePasswords("rawPassword", "encodedPassword"); + + assertTrue(result); + } +} diff --git a/target/classes/application.yml b/target/classes/application.yml new file mode 100644 index 0000000..bb7b15e --- /dev/null +++ b/target/classes/application.yml @@ -0,0 +1,45 @@ +app: + jwt: + secret: $2y$10$dJMr85s.761OiPWe.m2nRem3GCWiTWQmuPGY4MpayC7z6DH6LC4v2 + expirationMs: 300000 #5min + +spring: + application: + name: challenge + data: + mongodb: + uri: mongodb://localhost:27017/challenge_prod + security: + user: + name: admin + password: admin + #autoconfigure: + #exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration + kafka: + bootstrap-servers: localhost:9092 + consumer: + group-id: myGroup + auto-offset-reset: earliest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.apache.kafka.common.serialization.StringDeserializer + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.apache.kafka.common.serialization.StringSerializer +server: + port: 8082 + +logging: + level: + root: info + org: + springframework: + web: info + security: DEBUG + kafka: DEBUG + hibernate: error + +springdoc: + api-docs: + path: /v3/api-docs + swagger-ui: + path: /swagger-ui.html \ No newline at end of file diff --git a/target/classes/com/interbank/challenge/ChallengeApplication.class b/target/classes/com/interbank/challenge/ChallengeApplication.class new file mode 100644 index 0000000..39912ce Binary files /dev/null and b/target/classes/com/interbank/challenge/ChallengeApplication.class differ diff --git a/target/test-classes/com/interbank/challenge/ChallengeApplicationTests.class b/target/test-classes/com/interbank/challenge/ChallengeApplicationTests.class new file mode 100644 index 0000000..c4f975a Binary files /dev/null and b/target/test-classes/com/interbank/challenge/ChallengeApplicationTests.class differ