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