diff --git a/.gitignore b/.gitignore
index 67045665db..5a39ec4438 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,104 +1,91 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
-
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
-
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# TypeScript v1 declaration files
-typings/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
+# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,java
+# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,java
-# Optional eslint cache
-.eslintcache
+### Java ###
+# Compiled class file
+*.class
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
-.env
-.env.test
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-
-# Next.js build output
-.next
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and *not* Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
+# Log file
+*.log
-# TernJS port file
-.tern-port
+# BlueJ files
+*.ctxt
+
+# Mobile Tools for Java (J2ME)
+.mtj.tmp/
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+settings.json
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+replay_pid*
+
+### macOS ###
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Icon must end with two \r
+Icon
+
+
+# Thumbnails
+._*
+
+# Files that might appear in the root of a volume
+.DocumentRevisions-V100
+.fseventsd
+.Spotlight-V100
+.TemporaryItems
+.Trashes
+.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
+
+# Directories potentially created on remote AFP share
+.AppleDB
+.AppleDesktop
+Network Trash Folder
+Temporary Items
+.apdisk
+
+### macOS Patch ###
+# iCloud generated files
+*.icloud
+
+### Windows ###
+# Windows thumbnail cache files
+Thumbs.db
+Thumbs.db:encryptable
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+# End of https://www.toptal.com/developers/gitignore/api/macos,windows,java
\ No newline at end of file
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000000..26d33521af
--- /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 0000000000..6911f90bc6
--- /dev/null
+++ b/.idea/compiler.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000000..a4e550c2b9
--- /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 0000000000..712ab9d985
--- /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 0000000000..0b91961e06
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000000..b795c7fdb4
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000000..35eb1ddfbb
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/yape-codechallenge.iml b/.idea/yape-codechallenge.iml
new file mode 100644
index 0000000000..eccea1f39e
--- /dev/null
+++ b/.idea/yape-codechallenge.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docker-compose-local.yml b/docker-compose-local.yml
new file mode 100644
index 0000000000..a03ed6f0b6
--- /dev/null
+++ b/docker-compose-local.yml
@@ -0,0 +1,43 @@
+services:
+ postgres:
+ image: postgres:15
+ container_name: transactions-db
+ environment:
+ - POSTGRES_USER=postgres
+ - POSTGRES_PASSWORD=postgres
+ - POSTGRES_DB=transactions
+ ports:
+ - "5432:5432"
+
+ zookeeper:
+ image: confluentinc/cp-zookeeper:7.6.0
+ environment:
+ ZOOKEEPER_CLIENT_PORT: 2181
+ ports:
+ - "2181:2181"
+
+ kafka:
+ image: confluentinc/cp-kafka:7.6.0
+ depends_on:
+ - zookeeper
+ ports:
+ - "9092:9092"
+ environment:
+ KAFKA_BROKER_ID: 1
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+ KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ redis:
+ image: redis:7
+ container_name: transactions-redis
+ ports:
+ - "6379:6379"
+
+ redisinsight:
+ image: redis/redisinsight:latest
+ container_name: redisinsight
+ ports:
+ - "5540:5540"
+ depends_on:
+ - redis
diff --git a/docker-compose.yml b/docker-compose.yml
index 0e8807f21c..62534dbb86 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,25 +1,105 @@
-version: "3.7"
services:
+
postgres:
- image: postgres:14
+ image: postgres:15
+ container_name: transactions-db
+ environment:
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ POSTGRES_DB: transactions
ports:
- "5432:5432"
- environment:
- - POSTGRES_USER=postgres
- - POSTGRES_PASSWORD=postgres
+ networks:
+ - transactions-net
+
zookeeper:
- image: confluentinc/cp-zookeeper:5.5.3
+ image: confluentinc/cp-zookeeper:7.6.0
+ container_name: zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
+ ports:
+ - "2181:2181"
+ networks:
+ - transactions-net
+
kafka:
- image: confluentinc/cp-enterprise-kafka:5.5.3
- depends_on: [zookeeper]
+ image: confluentinc/cp-kafka:7.6.0
+ container_name: kafka
+ depends_on:
+ - zookeeper
+ ports:
+ - "9092:9092"
environment:
- KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
- KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
- KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_BROKER_ID: 1
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+
+ # 👇 IMPORTANTE
+ KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
+
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
- KAFKA_JMX_PORT: 9991
+ networks:
+ - transactions-net
+
+ redis:
+ image: redis:7
+ container_name: transactions-redis
+ ports:
+ - "6379:6379"
+ networks:
+ - transactions-net
+
+ redisinsight:
+ image: redis/redisinsight:latest
+ container_name: redisinsight
ports:
- - 9092:9092
+ - "5540:5540"
+ depends_on:
+ - redis
+ networks:
+ - transactions-net
+
+ productor-service:
+ build: ./yape-productor
+ container_name: productor-service
+ depends_on:
+ - postgres
+ - redis
+ - kafka
+ ports:
+ - "8080:8080"
+ environment:
+ SERVER_PORT: 8080
+
+ SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/transactions
+ SPRING_DATASOURCE_USERNAME: postgres
+ SPRING_DATASOURCE_PASSWORD: postgres
+
+ SPRING_REDIS_HOST: redis
+ SPRING_REDIS_PORT: 6379
+
+ SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
+ networks:
+ - transactions-net
+
+ antifraude-service:
+ build: ./yape-anti-fraude
+ container_name: antifraude-service
+ depends_on:
+ - kafka
+ - redis
+ ports:
+ - "8081:8081"
+ environment:
+ SERVER_PORT: 8081
+
+ SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092
+
+ SPRING_REDIS_HOST: redis
+ SPRING_REDIS_PORT: 6379
+ networks:
+ - transactions-net
+
+networks:
+ transactions-net:
+ driver: bridge
\ No newline at end of file
diff --git a/yape-anti-fraude/.gitattributes b/yape-anti-fraude/.gitattributes
new file mode 100644
index 0000000000..3b41682ac5
--- /dev/null
+++ b/yape-anti-fraude/.gitattributes
@@ -0,0 +1,2 @@
+/mvnw text eol=lf
+*.cmd text eol=crlf
diff --git a/yape-anti-fraude/.gitignore b/yape-anti-fraude/.gitignore
new file mode 100644
index 0000000000..667aaef0c8
--- /dev/null
+++ b/yape-anti-fraude/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/yape-anti-fraude/.mvn/wrapper/maven-wrapper.properties b/yape-anti-fraude/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000000..8dea6c227c
--- /dev/null
+++ b/yape-anti-fraude/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,3 @@
+wrapperVersion=3.3.4
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
diff --git a/yape-anti-fraude/Dockerfile b/yape-anti-fraude/Dockerfile
new file mode 100644
index 0000000000..9b3210c9c9
--- /dev/null
+++ b/yape-anti-fraude/Dockerfile
@@ -0,0 +1,18 @@
+FROM maven:3.9.6-eclipse-temurin-21 AS build
+
+WORKDIR /app
+
+COPY pom.xml .
+COPY src ./src
+
+RUN mvn clean package -DskipTests
+
+FROM eclipse-temurin:21-jdk-alpine
+
+WORKDIR /app
+
+COPY --from=build /app/target/*.jar app.jar
+
+EXPOSE 8081
+
+ENTRYPOINT ["java","-jar","app.jar"]
\ No newline at end of file
diff --git a/yape-anti-fraude/mvnw b/yape-anti-fraude/mvnw
new file mode 100755
index 0000000000..bd8896bf22
--- /dev/null
+++ b/yape-anti-fraude/mvnw
@@ -0,0 +1,295 @@
+#!/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
+#
+# http://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.4
+#
+# 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:]'
+}
+
+scriptDir="$(dirname "$0")"
+scriptName="$(basename "$0")"
+
+# 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 <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.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${scriptName#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
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+actualDistributionDir=""
+
+# First try the expected directory name (for regular distributions)
+if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
+ if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$distributionUrlNameMain"
+ fi
+fi
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if [ -z "$actualDistributionDir" ]; then
+ # enable globbing to iterate over items
+ set +f
+ for dir in "$TMP_DOWNLOAD_DIR"/*; do
+ if [ -d "$dir" ]; then
+ if [ -f "$dir/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$(basename "$dir")"
+ break
+ fi
+ fi
+ done
+ set -f
+fi
+
+if [ -z "$actualDistributionDir" ]; then
+ verbose "Contents of $TMP_DOWNLOAD_DIR:"
+ verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
+ die "Could not find Maven distribution directory in extracted archive"
+fi
+
+verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/yape-anti-fraude/mvnw.cmd b/yape-anti-fraude/mvnw.cmd
new file mode 100644
index 0000000000..92450f9327
--- /dev/null
+++ b/yape-anti-fraude/mvnw.cmd
@@ -0,0 +1,189 @@
+<# : 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 http://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.4
+@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 -eq $False) { "/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_M2_PATH = "$HOME/.m2"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
+}
+
+if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
+ New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
+}
+
+$MAVEN_WRAPPER_DISTS = $null
+if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
+ $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
+} else {
+ $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
+}
+
+$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::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
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+$actualDistributionDir = ""
+
+# First try the expected directory name (for regular distributions)
+$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
+$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
+if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
+ $actualDistributionDir = $distributionUrlNameMain
+}
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if (!$actualDistributionDir) {
+ Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
+ $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
+ if (Test-Path -Path $testPath -PathType Leaf) {
+ $actualDistributionDir = $_.Name
+ }
+ }
+}
+
+if (!$actualDistributionDir) {
+ Write-Error "Could not find Maven distribution directory in extracted archive"
+}
+
+Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -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/yape-anti-fraude/pom.xml b/yape-anti-fraude/pom.xml
new file mode 100644
index 0000000000..0ad02c3678
--- /dev/null
+++ b/yape-anti-fraude/pom.xml
@@ -0,0 +1,100 @@
+
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 4.0.3
+
+
+
+ com.kafka.anti-fraude
+ yape-anti-fraude
+ 0.0.1-SNAPSHOT
+ yape-anti-fraude
+ Yape Anti Fraude
+
+
+ 21
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-kafka
+
+
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+ com.fasterxml.jackson.core
+ jackson-core
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.38
+ provided
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+
+ org.springframework.kafka
+ spring-kafka-test
+ test
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 21
+ 21
+
+
+ org.projectlombok
+ lombok
+ 1.18.38
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
\ No newline at end of file
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/YapeAntiFraudeApplication.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/YapeAntiFraudeApplication.java
new file mode 100644
index 0000000000..79e8b689f6
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/YapeAntiFraudeApplication.java
@@ -0,0 +1,15 @@
+package com.kafka.anti_fraude;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.kafka.annotation.EnableKafka;
+
+@EnableKafka
+@SpringBootApplication()
+public class YapeAntiFraudeApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(YapeAntiFraudeApplication.class, args);
+ }
+
+}
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/service/AntiFraudService.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/service/AntiFraudService.java
new file mode 100644
index 0000000000..def75a008d
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/service/AntiFraudService.java
@@ -0,0 +1,25 @@
+package com.kafka.anti_fraude.application.service;
+
+import org.springframework.stereotype.Service;
+
+import com.kafka.anti_fraude.application.usecase.AntiFraudRuleUseCase;
+import com.kafka.anti_fraude.application.usecase.TransactionPublisherUseCase;
+import com.kafka.anti_fraude.application.usecase.ValidateTransactionUseCase;
+import com.kafka.anti_fraude.domain.command.TransactionValidationCommand;
+
+import lombok.RequiredArgsConstructor;
+
+@Service
+@RequiredArgsConstructor
+public class AntiFraudService implements ValidateTransactionUseCase {
+
+ private final AntiFraudRuleUseCase antiFraudRule;
+
+ private final TransactionPublisherUseCase statusPublisher;
+
+ @Override
+ public void execute(TransactionValidationCommand command) {
+ String newStatus = antiFraudRule.applyRule(command);
+ statusPublisher.publishStatusUpdate(command.transactionExternalId(), newStatus);
+ }
+}
\ No newline at end of file
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/usecase/AntiFraudRuleUseCase.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/usecase/AntiFraudRuleUseCase.java
new file mode 100644
index 0000000000..371dd78f7c
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/usecase/AntiFraudRuleUseCase.java
@@ -0,0 +1,7 @@
+package com.kafka.anti_fraude.application.usecase;
+
+import com.kafka.anti_fraude.domain.command.TransactionValidationCommand;
+
+public interface AntiFraudRuleUseCase {
+ String applyRule(TransactionValidationCommand command);
+}
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/usecase/TransactionPublisherUseCase.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/usecase/TransactionPublisherUseCase.java
new file mode 100644
index 0000000000..8509f4981c
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/usecase/TransactionPublisherUseCase.java
@@ -0,0 +1,9 @@
+package com.kafka.anti_fraude.application.usecase;
+
+import java.util.UUID;
+
+public interface TransactionPublisherUseCase {
+
+ void publishStatusUpdate(UUID transactionExternalId, String newStatus);
+
+}
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/usecase/ValidateTransactionUseCase.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/usecase/ValidateTransactionUseCase.java
new file mode 100644
index 0000000000..acda3e1d46
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/application/usecase/ValidateTransactionUseCase.java
@@ -0,0 +1,7 @@
+package com.kafka.anti_fraude.application.usecase;
+
+import com.kafka.anti_fraude.domain.command.TransactionValidationCommand;
+
+public interface ValidateTransactionUseCase {
+ void execute(TransactionValidationCommand command);
+}
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/domain/command/TransactionValidationCommand.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/domain/command/TransactionValidationCommand.java
new file mode 100644
index 0000000000..1d15c29e4f
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/domain/command/TransactionValidationCommand.java
@@ -0,0 +1,8 @@
+package com.kafka.anti_fraude.domain.command;
+
+import java.util.UUID;
+
+public record TransactionValidationCommand(
+ UUID transactionExternalId,
+ double value) {
+}
\ No newline at end of file
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/domain/model/Transaction.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/domain/model/Transaction.java
new file mode 100644
index 0000000000..a217f041d0
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/domain/model/Transaction.java
@@ -0,0 +1,38 @@
+// package com.kafka.anti_fraude.domain.model;
+
+// import java.time.Instant;
+// import java.util.UUID;
+
+// public record Transaction(
+// UUID transactionExternalId,
+// String accountExternalIdDebit,
+// String accountExternalIdCredit,
+// int tranferTypeId,
+// double value,
+// String transactionStatus,
+// Instant createdAt
+// ) {
+// public Transaction(String accountExternalIdDebit, String accountExternalIdCredit, int tranferTypeId, double value) {
+// this(
+// UUID.randomUUID(),
+// accountExternalIdDebit,
+// accountExternalIdCredit,
+// tranferTypeId,
+// value,
+// "pending",
+// Instant.now()
+// );
+// }
+
+// public Transaction updateStatus(String newStatus) {
+// return new Transaction(
+// this.transactionExternalId(),
+// this.accountExternalIdDebit(),
+// this.accountExternalIdCredit(),
+// this.tranferTypeId(),
+// this.value(),
+// newStatus,
+// this.createdAt()
+// );
+// }
+// }
\ No newline at end of file
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/AntiFraudRuleAdapter.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/AntiFraudRuleAdapter.java
new file mode 100644
index 0000000000..015100b14d
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/AntiFraudRuleAdapter.java
@@ -0,0 +1,17 @@
+package com.kafka.anti_fraude.infrastructure.messaging;
+
+import org.springframework.stereotype.Component;
+
+import com.kafka.anti_fraude.application.usecase.AntiFraudRuleUseCase;
+import com.kafka.anti_fraude.domain.command.TransactionValidationCommand;
+
+@Component
+public class AntiFraudRuleAdapter implements AntiFraudRuleUseCase {
+
+ private static final double VALUE = 1000.0;
+
+ @Override
+ public String applyRule(TransactionValidationCommand command) {
+ return (command.value() > VALUE) ? "REJECTED" : "APPROVED";
+ }
+}
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/config/JacksonConfig.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/config/JacksonConfig.java
new file mode 100644
index 0000000000..e33954412b
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/config/JacksonConfig.java
@@ -0,0 +1,19 @@
+package com.kafka.anti_fraude.infrastructure.messaging.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+@Configuration
+public class JacksonConfig {
+
+ @Bean
+ public ObjectMapper objectMapper() {
+ return new ObjectMapper()
+ .registerModule(new JavaTimeModule())
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ }
+}
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/dto/TransactionUpdatedEvent.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/dto/TransactionUpdatedEvent.java
new file mode 100644
index 0000000000..a27a1c407a
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/dto/TransactionUpdatedEvent.java
@@ -0,0 +1,8 @@
+package com.kafka.anti_fraude.infrastructure.messaging.dto;
+
+import java.util.UUID;
+
+public record TransactionUpdatedEvent(
+ UUID transactionExternalId,
+ String status
+) {}
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/event/TransactionCreatedEvent.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/event/TransactionCreatedEvent.java
new file mode 100644
index 0000000000..35a52de274
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/event/TransactionCreatedEvent.java
@@ -0,0 +1,14 @@
+package com.kafka.anti_fraude.infrastructure.messaging.event;
+
+import java.util.UUID;
+
+public record TransactionCreatedEvent(
+ UUID transactionExternalId,
+ // UUID accountExternalIdDebit,
+ // UUID accountExternalIdCredit,
+ // Long transactionTypeId,
+ // String transactionTypeName,
+ double value
+ // String status
+ // LocalDateTime createdAt
+) {}
\ No newline at end of file
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/kafka/KafkaTransactionPublisher.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/kafka/KafkaTransactionPublisher.java
new file mode 100644
index 0000000000..1692e19542
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/kafka/KafkaTransactionPublisher.java
@@ -0,0 +1,32 @@
+package com.kafka.anti_fraude.infrastructure.messaging.kafka;
+
+import java.util.UUID;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Component;
+
+import com.kafka.anti_fraude.application.usecase.TransactionPublisherUseCase;
+import com.kafka.anti_fraude.infrastructure.messaging.dto.TransactionUpdatedEvent;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class KafkaTransactionPublisher implements TransactionPublisherUseCase {
+
+ private final KafkaTemplate kafkaTemplate;
+
+ @Value("${app.kafka.topic-status-updated}")
+ private String topicUpdate;
+
+ @Override
+ public void publishStatusUpdate(UUID transactionExternalId, String newStatus) {
+ TransactionUpdatedEvent event = new TransactionUpdatedEvent(transactionExternalId, newStatus);
+ log.info("Publishing transaction status update event: {}", event);
+ kafkaTemplate.send(topicUpdate, transactionExternalId.toString(), event);
+
+ }
+}
diff --git a/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/kafka/TransactionCreatedListener.java b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/kafka/TransactionCreatedListener.java
new file mode 100644
index 0000000000..54732dcca2
--- /dev/null
+++ b/yape-anti-fraude/src/main/java/com/kafka/anti_fraude/infrastructure/messaging/kafka/TransactionCreatedListener.java
@@ -0,0 +1,32 @@
+package com.kafka.anti_fraude.infrastructure.messaging.kafka;
+
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+
+import com.kafka.anti_fraude.application.usecase.ValidateTransactionUseCase;
+import com.kafka.anti_fraude.domain.command.TransactionValidationCommand;
+import com.kafka.anti_fraude.infrastructure.messaging.event.TransactionCreatedEvent;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+public class TransactionCreatedListener {
+
+ private final ValidateTransactionUseCase validateTransactionUseCase;
+
+ public TransactionCreatedListener(ValidateTransactionUseCase validateTransactionUseCase) {
+ this.validateTransactionUseCase = validateTransactionUseCase;
+ }
+
+ @KafkaListener(topics = "${app.kafka.topic-created}", groupId = "antifraud-group")
+ public void listen(TransactionCreatedEvent event) {
+ log.info("Received transaction created event: {}", event);
+ TransactionValidationCommand command = new TransactionValidationCommand(
+ event.transactionExternalId(),
+ event.value()
+ );
+
+ validateTransactionUseCase.execute(command);
+ }
+}
\ No newline at end of file
diff --git a/yape-anti-fraude/src/main/resources/application.yaml b/yape-anti-fraude/src/main/resources/application.yaml
new file mode 100644
index 0000000000..6a60566da4
--- /dev/null
+++ b/yape-anti-fraude/src/main/resources/application.yaml
@@ -0,0 +1,29 @@
+spring:
+ port: 8081
+ application:
+ name: yape-anti-fraude
+ jackson:
+ serialization:
+ write-dates-as-timestamps: false
+ kafka:
+ bootstrap-servers: localhost:9092
+ consumer:
+ group-id: anti-fraud-group
+ auto-offset-reset: earliest
+ key-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
+ value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
+ properties:
+ spring.deserializer.key.delegate.class: org.apache.kafka.common.serialization.StringDeserializer
+ spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer
+ spring.json.trusted.packages: "*"
+ spring.json.use.type.headers: false
+ spring.json.value.default.type: com.kafka.anti_fraude.infrastructure.messaging.event.TransactionCreatedEvent
+
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
+
+app:
+ kafka:
+ topic-created: transactions.created
+ topic-status-updated: transactions.status-updated
\ No newline at end of file
diff --git a/yape-anti-fraude/src/main/resources/local.yaml b/yape-anti-fraude/src/main/resources/local.yaml
new file mode 100644
index 0000000000..80b32ca385
--- /dev/null
+++ b/yape-anti-fraude/src/main/resources/local.yaml
@@ -0,0 +1,34 @@
+server:
+ port: 8081
+
+spring:
+ application:
+ name: yape-anti-fraude
+
+ jackson:
+ serialization:
+ write-dates-as-timestamps: false
+
+ kafka:
+ bootstrap-servers: kafka:9092
+
+ consumer:
+ group-id: anti-fraud-group
+ auto-offset-reset: earliest
+ key-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
+ value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
+ properties:
+ spring.deserializer.key.delegate.class: org.apache.kafka.common.serialization.StringDeserializer
+ spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer
+ spring.json.trusted.packages: "*"
+ spring.json.use.type.headers: false
+ spring.json.value.default.type: com.kafka.anti_fraude.infrastructure.messaging.event.TransactionCreatedEvent
+
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
+
+app:
+ kafka:
+ topic-created: transactions.created
+ topic-status-updated: transactions.status-updated
\ No newline at end of file
diff --git a/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/YapeAntiFraudeApplicationTests.java b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/YapeAntiFraudeApplicationTests.java
new file mode 100644
index 0000000000..6746294578
--- /dev/null
+++ b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/YapeAntiFraudeApplicationTests.java
@@ -0,0 +1,13 @@
+package com.kafka.anti_fraude;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class YapeAntiFraudeApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/application/service/AntiFraudServiceTest.java b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/application/service/AntiFraudServiceTest.java
new file mode 100644
index 0000000000..7411af9730
--- /dev/null
+++ b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/application/service/AntiFraudServiceTest.java
@@ -0,0 +1,54 @@
+package com.kafka.anti_fraude.application.service;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.MockitoAnnotations;
+
+import com.kafka.anti_fraude.application.usecase.AntiFraudRuleUseCase;
+import com.kafka.anti_fraude.application.usecase.TransactionPublisherUseCase;
+import com.kafka.anti_fraude.domain.command.TransactionValidationCommand;
+
+public class AntiFraudServiceTest {
+ @Mock
+ private AntiFraudRuleUseCase antiFraudRule;
+
+ @Mock
+ private TransactionPublisherUseCase statusPublisher;
+
+ private AntiFraudService antiFraudService;
+
+ @BeforeEach
+ void setup() {
+ MockitoAnnotations.openMocks(this);
+ antiFraudService = new AntiFraudService(antiFraudRule, statusPublisher);
+ }
+
+ @Test
+ void execute_shouldPublishApprovedStatus_whenRuleReturnsApproved() {
+ UUID transactionId = UUID.randomUUID();
+ TransactionValidationCommand command = new TransactionValidationCommand(transactionId, 500.0);
+
+ when(antiFraudRule.applyRule(command)).thenReturn("APPROVED");
+
+ antiFraudService.execute(command);
+
+ verify(statusPublisher).publishStatusUpdate(transactionId, "APPROVED");
+ }
+
+ @Test
+ void execute_shouldPublishRejectedStatus_whenRuleReturnsRejected() {
+ UUID transactionId = UUID.randomUUID();
+ TransactionValidationCommand command = new TransactionValidationCommand(transactionId, 1500.0);
+
+ when(antiFraudRule.applyRule(command)).thenReturn("REJECTED");
+
+ antiFraudService.execute(command);
+
+ verify(statusPublisher).publishStatusUpdate(transactionId, "REJECTED");
+ }
+}
diff --git a/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/AntiFraudRuleAdapterTest.java b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/AntiFraudRuleAdapterTest.java
new file mode 100644
index 0000000000..e1f41a27ec
--- /dev/null
+++ b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/AntiFraudRuleAdapterTest.java
@@ -0,0 +1,25 @@
+package com.kafka.anti_fraude.infrastructure.messaging;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+
+import com.kafka.anti_fraude.domain.command.TransactionValidationCommand;
+
+class AntiFraudRuleAdapterTest {
+
+ private final AntiFraudRuleAdapter adapter = new AntiFraudRuleAdapter();
+
+ @Test
+ void applyRule_shouldReturnApproved_whenValueBelowThreshold() {
+ TransactionValidationCommand command = new TransactionValidationCommand(UUID.randomUUID(), 500.0);
+ assertEquals("APPROVED", adapter.applyRule(command));
+ }
+
+ @Test
+ void applyRule_shouldReturnRejected_whenValueAboveThreshold() {
+ TransactionValidationCommand command = new TransactionValidationCommand(UUID.randomUUID(), 1500.0);
+ assertEquals("REJECTED", adapter.applyRule(command));
+ }
+}
\ No newline at end of file
diff --git a/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/TransactionCreatedListenerTest.java b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/TransactionCreatedListenerTest.java
new file mode 100644
index 0000000000..7a98a5eebc
--- /dev/null
+++ b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/TransactionCreatedListenerTest.java
@@ -0,0 +1,35 @@
+package com.kafka.anti_fraude.infrastructure.messaging;
+
+import static org.mockito.Mockito.*;
+
+import com.kafka.anti_fraude.application.usecase.ValidateTransactionUseCase;
+import com.kafka.anti_fraude.infrastructure.messaging.event.TransactionCreatedEvent;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.UUID;
+
+import com.kafka.anti_fraude.infrastructure.messaging.kafka.TransactionCreatedListener;
+
+class TransactionCreatedListenerTest {
+
+ private ValidateTransactionUseCase validateTransactionUseCase;
+ private TransactionCreatedListener listener;
+
+ @BeforeEach
+ void setup() {
+ validateTransactionUseCase = mock(ValidateTransactionUseCase.class);
+ listener = new TransactionCreatedListener(validateTransactionUseCase);
+ }
+
+ @Test
+ void listen_shouldCallValidateTransactionUseCase() {
+ UUID transactionId = UUID.randomUUID();
+ TransactionCreatedEvent event = new TransactionCreatedEvent(transactionId, 200.0);
+
+ listener.listen(event);
+
+ verify(validateTransactionUseCase).execute(any());
+ }
+}
\ No newline at end of file
diff --git a/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/config/JacksonConfigTest.java b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/config/JacksonConfigTest.java
new file mode 100644
index 0000000000..6e096cc59a
--- /dev/null
+++ b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/config/JacksonConfigTest.java
@@ -0,0 +1,22 @@
+package com.kafka.anti_fraude.infrastructure.messaging.config;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+class JacksonConfigTest {
+
+ private final JacksonConfig config = new JacksonConfig();
+
+ @Test
+ void objectMapper_shouldDisableWriteDatesAsTimestamps() {
+ ObjectMapper mapper = config.objectMapper();
+
+ // Forma confiable de verificar SerializationFeature
+ boolean writeDatesAsTimestamps = mapper.getSerializationConfig()
+ .isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ assertFalse(writeDatesAsTimestamps, "WRITE_DATES_AS_TIMESTAMPS debe estar deshabilitado");
+ }
+}
\ No newline at end of file
diff --git a/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/kafka/KafkaTransactionPublisherTest.java b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/kafka/KafkaTransactionPublisherTest.java
new file mode 100644
index 0000000000..539e9038a9
--- /dev/null
+++ b/yape-anti-fraude/src/test/java/com/kafka/anti_fraude/infrastructure/messaging/kafka/KafkaTransactionPublisherTest.java
@@ -0,0 +1,35 @@
+package com.kafka.anti_fraude.infrastructure.messaging.kafka;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import com.kafka.anti_fraude.infrastructure.messaging.dto.TransactionUpdatedEvent;
+
+class KafkaTransactionPublisherTest {
+
+ private KafkaTemplate kafkaTemplate;
+ private KafkaTransactionPublisher publisher;
+
+ @BeforeEach
+ void setup() {
+ kafkaTemplate = mock(KafkaTemplate.class);
+ publisher = new KafkaTransactionPublisher(kafkaTemplate);
+ ReflectionTestUtils.setField(publisher, "topicUpdate", "test-topic");
+ }
+
+ @Test
+ void publishStatusUpdate_shouldSendEvent() {
+ UUID transactionId = UUID.randomUUID();
+ publisher.publishStatusUpdate(transactionId, "APPROVED");
+
+ verify(kafkaTemplate).send(eq("test-topic"), eq(transactionId.toString()), any(TransactionUpdatedEvent.class));
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/.gitattributes b/yape-productor/.gitattributes
new file mode 100644
index 0000000000..3b41682ac5
--- /dev/null
+++ b/yape-productor/.gitattributes
@@ -0,0 +1,2 @@
+/mvnw text eol=lf
+*.cmd text eol=crlf
diff --git a/yape-productor/.gitignore b/yape-productor/.gitignore
new file mode 100644
index 0000000000..667aaef0c8
--- /dev/null
+++ b/yape-productor/.gitignore
@@ -0,0 +1,33 @@
+HELP.md
+target/
+.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
diff --git a/yape-productor/.mvn/wrapper/maven-wrapper.properties b/yape-productor/.mvn/wrapper/maven-wrapper.properties
new file mode 100644
index 0000000000..8dea6c227c
--- /dev/null
+++ b/yape-productor/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,3 @@
+wrapperVersion=3.3.4
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
diff --git a/yape-productor/Dockerfile b/yape-productor/Dockerfile
new file mode 100644
index 0000000000..9eb0134ce3
--- /dev/null
+++ b/yape-productor/Dockerfile
@@ -0,0 +1,21 @@
+FROM maven:3.9.6-eclipse-temurin-21 AS build
+
+WORKDIR /app
+
+COPY pom.xml .
+COPY src ./src
+
+RUN mvn clean package -DskipTests
+
+# ========================
+# Stage 2 - Runtime
+# ========================
+FROM eclipse-temurin:21-jdk-alpine
+
+WORKDIR /app
+
+COPY --from=build /app/target/*.jar app.jar
+
+EXPOSE 8080
+
+ENTRYPOINT ["java","-jar","app.jar"]
\ No newline at end of file
diff --git a/yape-productor/mvnw b/yape-productor/mvnw
new file mode 100755
index 0000000000..bd8896bf22
--- /dev/null
+++ b/yape-productor/mvnw
@@ -0,0 +1,295 @@
+#!/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
+#
+# http://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.4
+#
+# 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:]'
+}
+
+scriptDir="$(dirname "$0")"
+scriptName="$(basename "$0")"
+
+# 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 <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.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${scriptName#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
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+actualDistributionDir=""
+
+# First try the expected directory name (for regular distributions)
+if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
+ if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$distributionUrlNameMain"
+ fi
+fi
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if [ -z "$actualDistributionDir" ]; then
+ # enable globbing to iterate over items
+ set +f
+ for dir in "$TMP_DOWNLOAD_DIR"/*; do
+ if [ -d "$dir" ]; then
+ if [ -f "$dir/bin/$MVN_CMD" ]; then
+ actualDistributionDir="$(basename "$dir")"
+ break
+ fi
+ fi
+ done
+ set -f
+fi
+
+if [ -z "$actualDistributionDir" ]; then
+ verbose "Contents of $TMP_DOWNLOAD_DIR:"
+ verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
+ die "Could not find Maven distribution directory in extracted archive"
+fi
+
+verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
+
+clean || :
+exec_maven "$@"
diff --git a/yape-productor/mvnw.cmd b/yape-productor/mvnw.cmd
new file mode 100644
index 0000000000..92450f9327
--- /dev/null
+++ b/yape-productor/mvnw.cmd
@@ -0,0 +1,189 @@
+<# : 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 http://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.4
+@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 -eq $False) { "/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_M2_PATH = "$HOME/.m2"
+if ($env:MAVEN_USER_HOME) {
+ $MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
+}
+
+if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
+ New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
+}
+
+$MAVEN_WRAPPER_DISTS = $null
+if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
+ $MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
+} else {
+ $MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
+}
+
+$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::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
+
+# Find the actual extracted directory name (handles snapshots where filename != directory name)
+$actualDistributionDir = ""
+
+# First try the expected directory name (for regular distributions)
+$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
+$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
+if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
+ $actualDistributionDir = $distributionUrlNameMain
+}
+
+# If not found, search for any directory with the Maven executable (for snapshots)
+if (!$actualDistributionDir) {
+ Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
+ $testPath = Join-Path $_.FullName "bin/$MVN_CMD"
+ if (Test-Path -Path $testPath -PathType Leaf) {
+ $actualDistributionDir = $_.Name
+ }
+ }
+}
+
+if (!$actualDistributionDir) {
+ Write-Error "Could not find Maven distribution directory in extracted archive"
+}
+
+Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -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/yape-productor/pom.xml b/yape-productor/pom.xml
new file mode 100644
index 0000000000..24a556cdf4
--- /dev/null
+++ b/yape-productor/pom.xml
@@ -0,0 +1,145 @@
+
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.2
+
+
+
+ com.kafka.productor
+ yape-productor
+ 0.0.1-SNAPSHOT
+ yape-productor
+ Yape Productor
+
+
+ 21
+ 1.6.3
+ 1.18.38
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ org.springframework
+ spring-messaging
+
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+ true
+
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+ provided
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.springframework.kafka
+ spring-kafka-test
+ test
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 21
+ 21
+
+ -parameters
+
+
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yape-productor/src/main/java/com/kafka/productor/YapeProductorApplication.java b/yape-productor/src/main/java/com/kafka/productor/YapeProductorApplication.java
new file mode 100644
index 0000000000..1384637134
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/YapeProductorApplication.java
@@ -0,0 +1,15 @@
+package com.kafka.productor;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.context.annotation.ComponentScan;
+
+@SpringBootApplication()
+@ComponentScan(basePackages = "com.kafka.productor")
+public class YapeProductorApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(YapeProductorApplication.class, args);
+ }
+
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/application/usecase/command/CreateTransactionCommand.java b/yape-productor/src/main/java/com/kafka/productor/application/usecase/command/CreateTransactionCommand.java
new file mode 100644
index 0000000000..d077b4a82f
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/application/usecase/command/CreateTransactionCommand.java
@@ -0,0 +1,16 @@
+package com.kafka.productor.application.usecase.command;
+
+import java.util.UUID;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+
+public record CreateTransactionCommand(
+ @NotNull UUID accountExternalIdDebit,
+
+ @NotNull UUID accountExternalIdCredit,
+
+ @NotNull Long transferTypeId,
+
+ @NotNull @Positive double value) {
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/create/CreateTransactionService.java b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/create/CreateTransactionService.java
new file mode 100644
index 0000000000..d1e68e5cf8
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/create/CreateTransactionService.java
@@ -0,0 +1,43 @@
+package com.kafka.productor.application.usecase.service.create;
+
+import java.util.UUID;
+
+import com.kafka.productor.application.usecase.command.CreateTransactionCommand;
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.domain.model.TransactionType;
+import com.kafka.productor.domain.port.EventPublisherPort;
+import com.kafka.productor.domain.port.TransactionRepositoryPort;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class CreateTransactionService implements CreateTransactionUseCase {
+
+ private final TransactionRepositoryPort repository;
+ private final EventPublisherPort eventPublisher;
+
+ public CreateTransactionService(
+ TransactionRepositoryPort repository,
+ EventPublisherPort eventPublisher) {
+ this.repository = repository;
+ this.eventPublisher = eventPublisher;
+ }
+
+ @Override
+ public UUID execute(CreateTransactionCommand command) {
+ UUID transactionExternalId = UUID.randomUUID();
+ Transaction transaction = new Transaction(
+ transactionExternalId,
+ command.accountExternalIdDebit(),
+ command.accountExternalIdCredit(),
+ new TransactionType(command.transferTypeId(), "TRANSFER"),
+ command.value());
+
+ log.info("Creating transaction with externalId: {}", transactionExternalId);
+ repository.save(transaction);
+
+ eventPublisher.publishTransactionCreated(transaction);
+
+ return transactionExternalId;
+ }
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/create/CreateTransactionUseCase.java b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/create/CreateTransactionUseCase.java
new file mode 100644
index 0000000000..7c6a7d9a82
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/create/CreateTransactionUseCase.java
@@ -0,0 +1,9 @@
+package com.kafka.productor.application.usecase.service.create;
+
+import java.util.UUID;
+
+import com.kafka.productor.application.usecase.command.CreateTransactionCommand;
+
+public interface CreateTransactionUseCase {
+ UUID execute(CreateTransactionCommand command);
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/get/GetTransactionService.java b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/get/GetTransactionService.java
new file mode 100644
index 0000000000..8965fd3ff8
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/get/GetTransactionService.java
@@ -0,0 +1,41 @@
+package com.kafka.productor.application.usecase.service.get;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import org.springframework.stereotype.Service;
+
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.domain.port.TransactionCachePort;
+import com.kafka.productor.domain.port.TransactionRepositoryPort;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class GetTransactionService implements GetTransactionUseCase {
+
+ private final TransactionRepositoryPort transactionRepositoryPort;
+ private final TransactionCachePort transactionCachePort;
+
+ @Override
+ public Optional execute(UUID transactionExternalId) {
+ log.info("GetTransactionService:execute: {}", transactionExternalId);
+ Optional cached = transactionCachePort.findById(transactionExternalId);
+ if (cached.isPresent()) {
+ return cached;
+ }
+
+ Optional transaction = transactionRepositoryPort
+ .findByTransactionExternalId(transactionExternalId);
+
+ log.info("GetTransactionService:transaction find: {}", transaction);
+
+ transaction.ifPresent(transactionCachePort::save);
+
+ return transaction;
+ }
+
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/get/GetTransactionUseCase.java b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/get/GetTransactionUseCase.java
new file mode 100644
index 0000000000..1c8a70167f
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/get/GetTransactionUseCase.java
@@ -0,0 +1,10 @@
+package com.kafka.productor.application.usecase.service.get;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import com.kafka.productor.domain.model.Transaction;
+
+public interface GetTransactionUseCase {
+ Optional execute(UUID transactionExternalId);
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/update/UpdateTransactionStatusService.java b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/update/UpdateTransactionStatusService.java
new file mode 100644
index 0000000000..9a67a34733
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/update/UpdateTransactionStatusService.java
@@ -0,0 +1,25 @@
+package com.kafka.productor.application.usecase.service.update;
+
+import java.util.UUID;
+
+import org.springframework.stereotype.Service;
+
+import com.kafka.productor.domain.port.TransactionCachePort;
+import com.kafka.productor.domain.port.TransactionRepositoryPort;
+
+import lombok.RequiredArgsConstructor;
+
+@Service
+@RequiredArgsConstructor
+public class UpdateTransactionStatusService implements UpdateTransactionStatusUseCase {
+
+ private final TransactionRepositoryPort transactionRepositoryPort;
+ private final TransactionCachePort transactionCachePort;
+
+ @Override
+ public void execute(UUID transactionExternalId, String status) {
+ transactionRepositoryPort.updateStatus(transactionExternalId, status);
+ transactionCachePort.delete(transactionExternalId);
+ }
+
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/update/UpdateTransactionStatusUseCase.java b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/update/UpdateTransactionStatusUseCase.java
new file mode 100644
index 0000000000..5f9b1585ff
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/application/usecase/service/update/UpdateTransactionStatusUseCase.java
@@ -0,0 +1,7 @@
+package com.kafka.productor.application.usecase.service.update;
+
+import java.util.UUID;
+
+public interface UpdateTransactionStatusUseCase {
+ void execute(UUID transactionExternalId, String status);
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/domain/model/Transaction.java b/yape-productor/src/main/java/com/kafka/productor/domain/model/Transaction.java
new file mode 100644
index 0000000000..0269d61fe4
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/domain/model/Transaction.java
@@ -0,0 +1,54 @@
+package com.kafka.productor.domain.model;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class Transaction {
+ private UUID transactionExternalId;
+ private UUID accountExternalIdDebit;
+ private UUID accountExternalIdCredit;
+ private TransactionType transactionType;
+ private TransactionStatus status;
+ private double value;
+ private LocalDateTime createdAt;
+
+ public Transaction() {
+ }
+
+ public Transaction(UUID transactionExternalId, double value) {
+ this.transactionExternalId = transactionExternalId;
+ this.value = value;
+ }
+
+ public Transaction(
+ UUID transactionExternalId,
+ UUID accountExternalIdDebit,
+ UUID accountExternalIdCredit,
+ TransactionType transactionType,
+ double value) {
+ this.transactionExternalId = transactionExternalId;
+ this.accountExternalIdDebit = accountExternalIdDebit;
+ this.accountExternalIdCredit = accountExternalIdCredit;
+ this.transactionType = transactionType;
+ this.value = value;
+ this.status = TransactionStatus.PENDING;
+ this.createdAt = LocalDateTime.now();
+ }
+
+ public void approve() {
+ this.status = TransactionStatus.APPROVED;
+ }
+
+ public void reject() {
+ this.status = TransactionStatus.REJECTED;
+ }
+
+ public void updateStatus(TransactionStatus status) {
+ this.status = status;
+ }
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/domain/model/TransactionStatus.java b/yape-productor/src/main/java/com/kafka/productor/domain/model/TransactionStatus.java
new file mode 100644
index 0000000000..f23548609c
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/domain/model/TransactionStatus.java
@@ -0,0 +1,7 @@
+package com.kafka.productor.domain.model;
+
+public enum TransactionStatus {
+ PENDING,
+ APPROVED,
+ REJECTED
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/domain/model/TransactionType.java b/yape-productor/src/main/java/com/kafka/productor/domain/model/TransactionType.java
new file mode 100644
index 0000000000..7ac011bea3
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/domain/model/TransactionType.java
@@ -0,0 +1,19 @@
+package com.kafka.productor.domain.model;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class TransactionType {
+
+ private Long id;
+ private String name;
+
+ public TransactionType() {}
+
+ public TransactionType(Long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/domain/port/EventListenerPort.java b/yape-productor/src/main/java/com/kafka/productor/domain/port/EventListenerPort.java
new file mode 100644
index 0000000000..c46c6b2842
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/domain/port/EventListenerPort.java
@@ -0,0 +1,7 @@
+package com.kafka.productor.domain.port;
+
+import com.kafka.productor.infrastructure.messaging.event.TransactionStatusUpdatedEvent;
+
+public interface EventListenerPort {
+ void publishTransactionStatusUpdated(TransactionStatusUpdatedEvent event);
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/domain/port/EventPublisherPort.java b/yape-productor/src/main/java/com/kafka/productor/domain/port/EventPublisherPort.java
new file mode 100644
index 0000000000..7f27c5ae8d
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/domain/port/EventPublisherPort.java
@@ -0,0 +1,7 @@
+package com.kafka.productor.domain.port;
+
+import com.kafka.productor.domain.model.Transaction;
+
+public interface EventPublisherPort {
+ void publishTransactionCreated(Transaction transaction);
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/domain/port/TransactionCachePort.java b/yape-productor/src/main/java/com/kafka/productor/domain/port/TransactionCachePort.java
new file mode 100644
index 0000000000..c259e43985
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/domain/port/TransactionCachePort.java
@@ -0,0 +1,14 @@
+package com.kafka.productor.domain.port;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import com.kafka.productor.domain.model.Transaction;
+
+public interface TransactionCachePort {
+ Optional findById(UUID transactionExternalId);
+
+ void save(Transaction transaction);
+
+ void delete(UUID transactionExternalId);
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/domain/port/TransactionRepositoryPort.java b/yape-productor/src/main/java/com/kafka/productor/domain/port/TransactionRepositoryPort.java
new file mode 100644
index 0000000000..834f911889
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/domain/port/TransactionRepositoryPort.java
@@ -0,0 +1,14 @@
+package com.kafka.productor.domain.port;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import com.kafka.productor.domain.model.Transaction;
+
+public interface TransactionRepositoryPort {
+ Transaction save(Transaction transaction);
+
+ Optional findByTransactionExternalId(UUID transactionExternalId);
+
+ void updateStatus(UUID transactionExternalId, String status);
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/cache/RedisTransactionCacheAdapter.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/cache/RedisTransactionCacheAdapter.java
new file mode 100644
index 0000000000..3bc77fabec
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/cache/RedisTransactionCacheAdapter.java
@@ -0,0 +1,49 @@
+package com.kafka.productor.infrastructure.cache;
+
+import java.time.Duration;
+import java.util.Optional;
+import java.util.UUID;
+
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.domain.port.TransactionCachePort;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class RedisTransactionCacheAdapter implements TransactionCachePort {
+
+ private final RedisTemplate redisTemplate;
+
+ private static final String PREFIX = "transaction:";
+
+ @Override
+ public Optional findById(UUID transactionExternalId) {
+ log.info("RedisTransactionCacheAdapter:findById: {}" + transactionExternalId);
+ Transaction transaction =
+ redisTemplate.opsForValue().get(PREFIX + transactionExternalId);
+
+ return Optional.ofNullable(transaction);
+ }
+
+ @Override
+ public void save(Transaction transaction) {
+ log.info("RedisTransactionCacheAdapter:save: {}" + transaction);
+ redisTemplate.opsForValue().set(
+ PREFIX + transaction.getTransactionExternalId(),
+ transaction,
+ Duration.ofMinutes(10)
+ );
+ }
+
+ @Override
+ public void delete(UUID transactionExternalId) {
+ redisTemplate.delete(PREFIX + transactionExternalId);
+ }
+
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/config/RedisConfig.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/config/RedisConfig.java
new file mode 100644
index 0000000000..4323726d09
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/config/RedisConfig.java
@@ -0,0 +1,36 @@
+package com.kafka.productor.infrastructure.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.kafka.productor.domain.model.Transaction;
+
+@Configuration
+public class RedisConfig {
+
+ @Bean
+ public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(connectionFactory);
+
+ Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Transaction.class);
+
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.registerModule(new JavaTimeModule());
+ serializer.setObjectMapper(mapper);
+
+ template.setKeySerializer(new StringRedisSerializer());
+ template.setValueSerializer(serializer);
+ template.setHashKeySerializer(new StringRedisSerializer());
+ template.setHashValueSerializer(serializer);
+
+ template.afterPropertiesSet();
+ return template;
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/config/UseCaseConfig.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/config/UseCaseConfig.java
new file mode 100644
index 0000000000..5349469c22
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/config/UseCaseConfig.java
@@ -0,0 +1,21 @@
+package com.kafka.productor.infrastructure.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import com.kafka.productor.application.usecase.service.create.CreateTransactionService;
+import com.kafka.productor.application.usecase.service.create.CreateTransactionUseCase;
+import com.kafka.productor.domain.port.EventPublisherPort;
+import com.kafka.productor.domain.port.TransactionRepositoryPort;
+
+@Configuration
+public class UseCaseConfig {
+
+ @Bean
+ public CreateTransactionUseCase createTransactionUseCase(
+ TransactionRepositoryPort repository,
+ EventPublisherPort eventPublisher) {
+
+ return new CreateTransactionService(repository, eventPublisher);
+ }
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/TransactionController.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/TransactionController.java
new file mode 100644
index 0000000000..ba372519bb
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/TransactionController.java
@@ -0,0 +1,50 @@
+package com.kafka.productor.infrastructure.controller;
+
+import java.util.UUID;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import com.kafka.productor.application.usecase.service.create.CreateTransactionUseCase;
+import com.kafka.productor.application.usecase.service.get.GetTransactionUseCase;
+import com.kafka.productor.infrastructure.controller.dto.CreateTransactionRequest;
+import com.kafka.productor.infrastructure.controller.dto.CreateTransactionResponse;
+import com.kafka.productor.infrastructure.controller.dto.TransactionResponse;
+
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+
+@RestController
+@RequestMapping("/transactions")
+@RequiredArgsConstructor
+public class TransactionController {
+
+ private final CreateTransactionUseCase createTransactionUseCase;
+ private final GetTransactionUseCase getTransactionUseCase;
+
+ @PostMapping
+ public ResponseEntity create(
+ @Valid @RequestBody CreateTransactionRequest request) {
+
+ UUID transactionExternalId = createTransactionUseCase.execute(request.toCommand());
+
+ return ResponseEntity
+ .status(HttpStatus.CREATED)
+ .body(new CreateTransactionResponse(transactionExternalId));
+ }
+
+ @GetMapping("/{transactionExternalId}")
+ public ResponseEntity get(
+ @PathVariable UUID transactionExternalId) {
+
+ return getTransactionUseCase.execute(transactionExternalId)
+ .map(transaction -> ResponseEntity.ok(TransactionResponse.from(transaction)))
+ .orElse(ResponseEntity.notFound().build());
+ }
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionRequest.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionRequest.java
new file mode 100644
index 0000000000..f17c8d09f1
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionRequest.java
@@ -0,0 +1,28 @@
+package com.kafka.productor.infrastructure.controller.dto;
+
+import java.util.UUID;
+
+import com.kafka.productor.application.usecase.command.CreateTransactionCommand;
+
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+
+public record CreateTransactionRequest(
+ @NotNull UUID accountExternalIdDebit,
+
+ @NotNull UUID accountExternalIdCredit,
+
+ @NotNull Long transferTypeId,
+
+ @NotNull @Positive double value
+
+) {
+
+ public CreateTransactionCommand toCommand() {
+ return new CreateTransactionCommand(
+ accountExternalIdDebit,
+ accountExternalIdCredit,
+ transferTypeId,
+ value);
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionResponse.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionResponse.java
new file mode 100644
index 0000000000..82449bcaa3
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionResponse.java
@@ -0,0 +1,7 @@
+package com.kafka.productor.infrastructure.controller.dto;
+
+import java.util.UUID;
+
+public record CreateTransactionResponse(UUID transactionExternalId) {
+
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/dto/TransactionResponse.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/dto/TransactionResponse.java
new file mode 100644
index 0000000000..ba5b1c9478
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/controller/dto/TransactionResponse.java
@@ -0,0 +1,32 @@
+package com.kafka.productor.infrastructure.controller.dto;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import com.kafka.productor.domain.model.Transaction;
+
+public record TransactionResponse(
+ UUID transactionExternalId,
+ TransactionTypeResponse transactionType,
+ TransactionStatusResponse transactionStatus,
+ double value,
+ LocalDateTime createdAt
+) {
+
+ public static TransactionResponse from(Transaction transaction) {
+ return new TransactionResponse(
+ transaction.getTransactionExternalId(),
+ new TransactionTypeResponse(
+ transaction.getTransactionType().getName()
+ ),
+ new TransactionStatusResponse(
+ transaction.getStatus().name()
+ ),
+ transaction.getValue(),
+ transaction.getCreatedAt()
+ );
+ }
+
+ public record TransactionTypeResponse(String name) {}
+ public record TransactionStatusResponse(String name) {}
+}
\ No newline at end of file
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/mapper/TransactionMapperImpl.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/mapper/TransactionMapperImpl.java
new file mode 100644
index 0000000000..cad01d3d9b
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/mapper/TransactionMapperImpl.java
@@ -0,0 +1,72 @@
+package com.kafka.productor.infrastructure.mapper;
+
+import org.springframework.stereotype.Component;
+
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.domain.model.TransactionStatus;
+import com.kafka.productor.domain.model.TransactionType;
+import com.kafka.productor.infrastructure.persistence.entity.TransactionEntity;
+
+@Component
+public class TransactionMapperImpl implements TransactionMapperInterface {
+
+ @Override
+ public TransactionEntity toEntity(Transaction transaction) {
+
+ if (transaction == null) {
+ return null;
+ }
+
+ TransactionEntity entity = new TransactionEntity();
+
+ entity.setTransactionExternalId(transaction.getTransactionExternalId());
+ entity.setAccountExternalIdDebit(transaction.getAccountExternalIdDebit());
+ entity.setAccountExternalIdCredit(transaction.getAccountExternalIdCredit());
+ entity.setTransactionTypeId(
+ transaction.getTransactionType() != null
+ ? transaction.getTransactionType().getId()
+ : null
+ );
+ entity.setTransactionTypeName(
+ transaction.getTransactionType() != null
+ ? transaction.getTransactionType().getName()
+ : null
+ );
+ entity.setStatus(
+ transaction.getStatus() != null
+ ? transaction.getStatus().name()
+ : null
+ );
+ entity.setValue(transaction.getValue());
+ entity.setCreatedAt(transaction.getCreatedAt());
+
+ return entity;
+ }
+
+ @Override
+ public Transaction toDomain(TransactionEntity entity) {
+
+ if (entity == null) {
+ return null;
+ }
+
+ TransactionType transactionType = new TransactionType(
+ entity.getTransactionTypeId(),
+ entity.getTransactionTypeName()
+ );
+
+ Transaction transaction = new Transaction(
+ entity.getTransactionExternalId(),
+ entity.getAccountExternalIdDebit(),
+ entity.getAccountExternalIdCredit(),
+ transactionType,
+ entity.getValue()
+ );
+
+ if (entity.getStatus() != null) {
+ transaction.setStatus(TransactionStatus.valueOf(entity.getStatus()));
+ }
+
+ return transaction;
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/mapper/TransactionMapperInterface.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/mapper/TransactionMapperInterface.java
new file mode 100644
index 0000000000..4afe195a1c
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/mapper/TransactionMapperInterface.java
@@ -0,0 +1,10 @@
+package com.kafka.productor.infrastructure.mapper;
+
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.infrastructure.persistence.entity.TransactionEntity;
+
+public interface TransactionMapperInterface {
+ TransactionEntity toEntity(Transaction transaction);
+
+ Transaction toDomain(TransactionEntity entity);
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/event/TransactionStatusUpdatedEvent.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/event/TransactionStatusUpdatedEvent.java
new file mode 100644
index 0000000000..844c75c951
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/event/TransactionStatusUpdatedEvent.java
@@ -0,0 +1,8 @@
+package com.kafka.productor.infrastructure.messaging.event;
+
+import java.util.UUID;
+
+public record TransactionStatusUpdatedEvent(
+ UUID transactionExternalId,
+ String status) {
+}
\ No newline at end of file
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionCreatedEvent.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionCreatedEvent.java
new file mode 100644
index 0000000000..b3057964eb
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionCreatedEvent.java
@@ -0,0 +1,16 @@
+package com.kafka.productor.infrastructure.messaging.kafka;
+
+import java.util.UUID;
+
+import com.kafka.productor.domain.model.Transaction;
+
+public record TransactionCreatedEvent(
+ UUID transactionExternalId,
+ double value) {
+
+ public static TransactionCreatedEvent from(Transaction transaction) {
+ return new TransactionCreatedEvent(
+ transaction.getTransactionExternalId(),
+ transaction.getValue());
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaListener.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaListener.java
new file mode 100644
index 0000000000..6bb64517fd
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaListener.java
@@ -0,0 +1,27 @@
+package com.kafka.productor.infrastructure.messaging.kafka;
+
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+
+import com.kafka.productor.application.usecase.service.update.UpdateTransactionStatusUseCase;
+import com.kafka.productor.domain.port.EventListenerPort;
+import com.kafka.productor.infrastructure.messaging.event.TransactionStatusUpdatedEvent;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TransactionKafkaListener implements EventListenerPort {
+
+ private final UpdateTransactionStatusUseCase updateTransactionStatusUseCase;
+
+ @Override
+ @KafkaListener(topics = "${app.kafka.topic-status-updated}", groupId = "antifraud-group")
+ public void publishTransactionStatusUpdated(TransactionStatusUpdatedEvent event) {
+ log.info("publishTransactionStatusUpdated: {}: {}",
+ event.transactionExternalId(), event.status());
+ updateTransactionStatusUseCase.execute(event.transactionExternalId(), event.status());
+ }
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaProducer.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaProducer.java
new file mode 100644
index 0000000000..3bcdcd6658
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaProducer.java
@@ -0,0 +1,27 @@
+package com.kafka.productor.infrastructure.messaging.kafka;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Component;
+
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.domain.port.EventPublisherPort;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TransactionKafkaProducer implements EventPublisherPort {
+
+ private final KafkaTemplate kafkaTemplate;
+
+ @Value("${app.kafka.topic-created}")
+ private String topicCreated;
+
+ @Override
+ public void publishTransactionCreated(Transaction transaction) {
+ kafkaTemplate.send(topicCreated, transaction.getTransactionExternalId().toString(), transaction);
+ }
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/persistence/adapter/TransactionRepositoryAdapter.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/persistence/adapter/TransactionRepositoryAdapter.java
new file mode 100644
index 0000000000..6be942d9a6
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/persistence/adapter/TransactionRepositoryAdapter.java
@@ -0,0 +1,44 @@
+package com.kafka.productor.infrastructure.persistence.adapter;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import org.springframework.stereotype.Component;
+
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.domain.port.TransactionRepositoryPort;
+import com.kafka.productor.infrastructure.mapper.TransactionMapperInterface;
+import com.kafka.productor.infrastructure.persistence.entity.TransactionEntity;
+import com.kafka.productor.infrastructure.persistence.repository.JpaTransactionRepository;
+
+import lombok.RequiredArgsConstructor;
+
+@Component
+@RequiredArgsConstructor
+public class TransactionRepositoryAdapter implements TransactionRepositoryPort {
+
+ private final JpaTransactionRepository repository;
+ private final TransactionMapperInterface mapper;
+
+ @Override
+ public Transaction save(Transaction transaction) {
+ TransactionEntity entity = mapper.toEntity(transaction);
+ repository.save(entity);
+ return transaction;
+ }
+
+ @Override
+ public Optional findByTransactionExternalId(UUID transactionExternalId) {
+ return repository.findByTransactionExternalId(transactionExternalId)
+ .map(mapper::toDomain);
+ }
+
+ @Override
+ public void updateStatus(UUID transactionExternalId, String status) {
+ TransactionEntity entity = repository.findByTransactionExternalId(transactionExternalId)
+ .orElseThrow();
+
+ entity.setStatus(status);
+ repository.save(entity);
+ }
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/persistence/entity/TransactionEntity.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/persistence/entity/TransactionEntity.java
new file mode 100644
index 0000000000..86ad361b07
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/persistence/entity/TransactionEntity.java
@@ -0,0 +1,40 @@
+package com.kafka.productor.infrastructure.persistence.entity;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+@Entity
+@Table(name = "transactions")
+public class TransactionEntity {
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(unique = true, nullable = false)
+ private UUID transactionExternalId;
+
+ private UUID accountExternalIdDebit;
+
+ private UUID accountExternalIdCredit;
+
+ private Long transactionTypeId;
+
+ private String transactionTypeName;
+
+ private String status;
+
+ private double value;
+
+ private LocalDateTime createdAt;
+}
diff --git a/yape-productor/src/main/java/com/kafka/productor/infrastructure/persistence/repository/JpaTransactionRepository.java b/yape-productor/src/main/java/com/kafka/productor/infrastructure/persistence/repository/JpaTransactionRepository.java
new file mode 100644
index 0000000000..5a76c2af58
--- /dev/null
+++ b/yape-productor/src/main/java/com/kafka/productor/infrastructure/persistence/repository/JpaTransactionRepository.java
@@ -0,0 +1,13 @@
+package com.kafka.productor.infrastructure.persistence.repository;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import com.kafka.productor.infrastructure.persistence.entity.TransactionEntity;
+
+public interface JpaTransactionRepository extends JpaRepository {
+
+ Optional findByTransactionExternalId(UUID transactionExternalId);
+}
diff --git a/yape-productor/src/main/resources/application.yaml b/yape-productor/src/main/resources/application.yaml
new file mode 100644
index 0000000000..cd775bb2f8
--- /dev/null
+++ b/yape-productor/src/main/resources/application.yaml
@@ -0,0 +1,63 @@
+server:
+ port: 8080
+
+spring:
+ application:
+ name: yape-productor
+
+ data:
+ redis:
+ host: redis
+ port: 6379
+ timeout: 60000
+
+ datasource:
+ url: jdbc:postgresql://postgres:5432/transactions
+ username: postgres
+ password: postgres
+
+ jpa:
+ hibernate:
+ ddl-auto: update
+ show-sql: false
+
+ kafka:
+ bootstrap-servers: kafka:9092
+
+ admin:
+ auto-create: true
+
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
+ properties:
+ spring.json.add.type.headers: false
+ acks: all
+ retries: 10
+
+ consumer:
+ group-id: anti-fraud-group
+ auto-offset-reset: earliest
+ key-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
+ value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
+ properties:
+ spring.deserializer.key.delegate.class: org.apache.kafka.common.serialization.StringDeserializer
+ spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer
+ spring.json.trusted.packages: "*"
+ spring.json.use.type.headers: false
+ spring.json.value.default.type: com.kafka.productor.infrastructure.messaging.event.TransactionStatusUpdatedEvent
+
+logging:
+ level:
+ org.hibernate.SQL: debug
+
+app:
+ kafka:
+ topic-created: transactions.created
+ topic-status-updated: transactions.status-updated
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: mappings
\ No newline at end of file
diff --git a/yape-productor/src/main/resources/local.yml b/yape-productor/src/main/resources/local.yml
new file mode 100644
index 0000000000..d3a8f578ac
--- /dev/null
+++ b/yape-productor/src/main/resources/local.yml
@@ -0,0 +1,62 @@
+server:
+ port: 8080
+
+spring:
+ application:
+ name: yape-productor
+
+ data:
+ redis:
+ host: localhost
+ port: 6379
+ timeout: 60000
+
+ datasource:
+ url: jdbc:postgresql://localhost:5432/transactions
+ username: postgres
+ password: postgres
+
+ jpa:
+ hibernate:
+ ddl-auto: update
+ show-sql: false
+
+ kafka:
+ bootstrap-servers: localhost:9092
+ admin:
+ auto-create: true
+
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
+ properties:
+ spring.json.add.type.headers: false
+ acks: all
+ retries: 10
+
+ consumer:
+ group-id: anti-fraud-group
+ auto-offset-reset: earliest
+ key-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
+ value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer
+ properties:
+ spring.deserializer.key.delegate.class: org.apache.kafka.common.serialization.StringDeserializer
+ spring.deserializer.value.delegate.class: org.springframework.kafka.support.serializer.JsonDeserializer
+ spring.json.trusted.packages: "*"
+ spring.json.use.type.headers: false
+ spring.json.value.default.type: com.kafka.productor.infrastructure.messaging.event.TransactionStatusUpdatedEvent
+
+logging:
+ level:
+ org.hibernate.SQL: debug
+
+app:
+ kafka:
+ topic-created: transactions.created
+ topic-status-updated: transactions.status-updated
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: mappings
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/YapeProductorApplicationTests.java b/yape-productor/src/test/java/com/kafka/productor/YapeProductorApplicationTests.java
new file mode 100644
index 0000000000..65a92b27b3
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/YapeProductorApplicationTests.java
@@ -0,0 +1,13 @@
+package com.kafka.productor;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class YapeProductorApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/yape-productor/src/test/java/com/kafka/productor/application/usecase/service/create/CreateTransactionServiceTest.java b/yape-productor/src/test/java/com/kafka/productor/application/usecase/service/create/CreateTransactionServiceTest.java
new file mode 100644
index 0000000000..de40923cc0
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/application/usecase/service/create/CreateTransactionServiceTest.java
@@ -0,0 +1,53 @@
+package com.kafka.productor.application.usecase.service.create;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.kafka.productor.application.usecase.command.CreateTransactionCommand;
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.domain.port.EventPublisherPort;
+import com.kafka.productor.domain.port.TransactionRepositoryPort;
+
+class CreateTransactionServiceTest {
+
+ private TransactionRepositoryPort repository;
+ private EventPublisherPort eventPublisher;
+ private CreateTransactionService service;
+
+ @BeforeEach
+ void setup() {
+ repository = mock(TransactionRepositoryPort.class);
+ eventPublisher = mock(EventPublisherPort.class);
+ service = new CreateTransactionService(repository, eventPublisher);
+ }
+
+ @Test
+ void execute_shouldCreateTransactionAndPublishEvent() {
+ CreateTransactionCommand command = new CreateTransactionCommand(
+ UUID.randomUUID(), UUID.randomUUID(), 1L, 1000.0
+ );
+
+ UUID externalId = service.execute(command);
+
+ assertNotNull(externalId);
+
+ ArgumentCaptor transactionCaptor = ArgumentCaptor.forClass(Transaction.class);
+ verify(repository).save(transactionCaptor.capture());
+ Transaction saved = transactionCaptor.getValue();
+
+ assertEquals(externalId, saved.getTransactionExternalId());
+ assertEquals(command.accountExternalIdDebit(), saved.getAccountExternalIdDebit());
+ assertEquals(command.accountExternalIdCredit(), saved.getAccountExternalIdCredit());
+ assertEquals(command.transferTypeId(), saved.getTransactionType().getId());
+ assertEquals(command.value(), saved.getValue());
+
+ verify(eventPublisher).publishTransactionCreated(saved);
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/application/usecase/service/get/GetTransactionServiceTest.java b/yape-productor/src/test/java/com/kafka/productor/application/usecase/service/get/GetTransactionServiceTest.java
new file mode 100644
index 0000000000..a3a530f2d5
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/application/usecase/service/get/GetTransactionServiceTest.java
@@ -0,0 +1,79 @@
+package com.kafka.productor.application.usecase.service.get;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.domain.model.TransactionType;
+import com.kafka.productor.domain.port.TransactionCachePort;
+import com.kafka.productor.domain.port.TransactionRepositoryPort;
+
+class GetTransactionServiceTest {
+
+ private TransactionRepositoryPort repository;
+ private TransactionCachePort cache;
+ private GetTransactionService service;
+
+ @BeforeEach
+ void setup() {
+ repository = mock(TransactionRepositoryPort.class);
+ cache = mock(TransactionCachePort.class);
+ service = new GetTransactionService(repository, cache);
+ }
+
+ @Test
+ void execute_shouldReturnFromCache_ifPresent() {
+ UUID id = UUID.randomUUID();
+ Transaction transaction = new Transaction(id, UUID.randomUUID(), UUID.randomUUID(),
+ new TransactionType(1L, "TRANSFER"), 100.0);
+
+ when(cache.findById(id)).thenReturn(Optional.of(transaction));
+
+ Optional result = service.execute(id);
+
+ assertTrue(result.isPresent());
+ assertEquals(transaction, result.get());
+
+ verify(repository, never()).findByTransactionExternalId(any());
+ }
+
+ @Test
+ void execute_shouldReturnFromRepository_ifCacheEmpty() {
+ UUID id = UUID.randomUUID();
+ Transaction transaction = new Transaction(id, UUID.randomUUID(), UUID.randomUUID(),
+ new TransactionType(1L, "TRANSFER"), 200.0);
+
+ when(cache.findById(id)).thenReturn(Optional.empty());
+ when(repository.findByTransactionExternalId(id)).thenReturn(Optional.of(transaction));
+
+ Optional result = service.execute(id);
+
+ assertTrue(result.isPresent());
+ assertEquals(transaction, result.get());
+
+ verify(cache).save(transaction);
+ }
+
+ @Test
+ void execute_shouldReturnEmpty_ifNotFoundAnywhere() {
+ UUID id = UUID.randomUUID();
+
+ when(cache.findById(id)).thenReturn(Optional.empty());
+ when(repository.findByTransactionExternalId(id)).thenReturn(Optional.empty());
+
+ Optional result = service.execute(id);
+
+ assertTrue(result.isEmpty());
+ verify(cache, never()).save(any());
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/application/usecase/service/update/UpdateTransactionStatusServiceTest.java b/yape-productor/src/test/java/com/kafka/productor/application/usecase/service/update/UpdateTransactionStatusServiceTest.java
new file mode 100644
index 0000000000..67bc376008
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/application/usecase/service/update/UpdateTransactionStatusServiceTest.java
@@ -0,0 +1,37 @@
+package com.kafka.productor.application.usecase.service.update;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import com.kafka.productor.domain.port.TransactionCachePort;
+import com.kafka.productor.domain.port.TransactionRepositoryPort;
+
+class UpdateTransactionStatusServiceTest {
+
+ private TransactionRepositoryPort repository;
+ private TransactionCachePort cache;
+ private UpdateTransactionStatusService service;
+
+ @BeforeEach
+ void setup() {
+ repository = mock(TransactionRepositoryPort.class);
+ cache = mock(TransactionCachePort.class);
+ service = new UpdateTransactionStatusService(repository, cache);
+ }
+
+ @Test
+ void execute_shouldUpdateStatusAndDeleteFromCache() {
+ UUID id = UUID.randomUUID();
+ String status = "APPROVED";
+
+ service.execute(id, status);
+
+ verify(repository).updateStatus(id, status);
+
+ verify(cache).delete(id);
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/domain/model/TransactionTest.java b/yape-productor/src/test/java/com/kafka/productor/domain/model/TransactionTest.java
new file mode 100644
index 0000000000..19f904d02b
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/domain/model/TransactionTest.java
@@ -0,0 +1,87 @@
+package com.kafka.productor.domain.model;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+
+class TransactionTest {
+
+ @Test
+ void testDefaultConstructor() {
+ Transaction tx = new Transaction();
+ assertNull(tx.getTransactionExternalId());
+ assertNull(tx.getAccountExternalIdDebit());
+ assertNull(tx.getAccountExternalIdCredit());
+ assertNull(tx.getTransactionType());
+ assertNull(tx.getStatus());
+ assertEquals(0.0, tx.getValue());
+ assertNull(tx.getCreatedAt());
+ }
+
+ @Test
+ void testConstructorWithIdAndValue() {
+ UUID id = UUID.randomUUID();
+ double value = 123.45;
+ Transaction tx = new Transaction(id, value);
+
+ assertEquals(id, tx.getTransactionExternalId());
+ assertEquals(value, tx.getValue());
+ assertNull(tx.getAccountExternalIdDebit());
+ assertNull(tx.getAccountExternalIdCredit());
+ assertNull(tx.getTransactionType());
+ assertNull(tx.getStatus());
+ assertNull(tx.getCreatedAt());
+ }
+
+ @Test
+ void testFullConstructor() {
+ UUID idDebit = UUID.randomUUID();
+ UUID idCredit = UUID.randomUUID();
+ UUID transactionId = UUID.randomUUID();
+ double value = 500.0;
+ TransactionType type = new TransactionType(1L, "TRANSFER");
+
+ Transaction tx = new Transaction(transactionId, idDebit, idCredit, type, value);
+
+ assertEquals(transactionId, tx.getTransactionExternalId());
+ assertEquals(idDebit, tx.getAccountExternalIdDebit());
+ assertEquals(idCredit, tx.getAccountExternalIdCredit());
+ assertEquals(type, tx.getTransactionType());
+ assertEquals(TransactionStatus.PENDING, tx.getStatus());
+ assertEquals(value, tx.getValue());
+ assertNotNull(tx.getCreatedAt());
+ assertTrue(tx.getCreatedAt().isBefore(LocalDateTime.now().plusSeconds(1)));
+ }
+
+ @Test
+ void testApproveRejectUpdateStatus() {
+ Transaction tx = new Transaction(UUID.randomUUID(), 100.0);
+ tx.approve();
+ assertEquals(TransactionStatus.APPROVED, tx.getStatus());
+
+ tx.reject();
+ assertEquals(TransactionStatus.REJECTED, tx.getStatus());
+
+ tx.updateStatus(TransactionStatus.PENDING);
+ assertEquals(TransactionStatus.PENDING, tx.getStatus());
+ }
+
+ @Test
+ void testTransactionTypeGettersSetters() {
+ TransactionType type = new TransactionType();
+ type.setId(10L);
+ type.setName("TRANSFER");
+
+ assertEquals(10L, type.getId());
+ assertEquals("TRANSFER", type.getName());
+
+ TransactionType type2 = new TransactionType(20L, "DEPOSIT");
+ assertEquals(20L, type2.getId());
+ assertEquals("DEPOSIT", type2.getName());
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/cache/RedisTransactionCacheAdapterTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/cache/RedisTransactionCacheAdapterTest.java
new file mode 100644
index 0000000000..0cf92fafd8
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/cache/RedisTransactionCacheAdapterTest.java
@@ -0,0 +1,90 @@
+package com.kafka.productor.infrastructure.cache;
+
+import java.time.Duration;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.eq;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ValueOperations;
+
+import com.kafka.productor.domain.model.Transaction;
+
+@ExtendWith(MockitoExtension.class)
+class RedisTransactionCacheAdapterTest {
+
+ @Mock
+ private RedisTemplate redisTemplate;
+
+ @Mock
+ private ValueOperations valueOperations;
+
+ @InjectMocks
+ private RedisTransactionCacheAdapter adapter;
+
+ private static final String PREFIX = "transaction:";
+
+ @Test
+ void shouldReturnTransactionWhenExistsInCache() {
+ UUID id = UUID.randomUUID();
+ Transaction transaction = mock(Transaction.class);
+
+ when(redisTemplate.opsForValue()).thenReturn(valueOperations);
+ when(valueOperations.get(PREFIX + id)).thenReturn(transaction);
+
+ Optional result = adapter.findById(id);
+
+ assertTrue(result.isPresent());
+ assertEquals(transaction, result.get());
+ }
+
+ @Test
+ void shouldReturnEmptyWhenTransactionNotInCache() {
+ UUID id = UUID.randomUUID();
+
+ when(redisTemplate.opsForValue()).thenReturn(valueOperations);
+ when(valueOperations.get(PREFIX + id)).thenReturn(null);
+
+ Optional result = adapter.findById(id);
+
+ assertTrue(result.isEmpty());
+ }
+
+ @Test
+ void shouldSaveTransactionWithTTL() {
+ Transaction transaction = mock(Transaction.class);
+ UUID id = UUID.randomUUID();
+
+ when(transaction.getTransactionExternalId()).thenReturn(id);
+ when(redisTemplate.opsForValue()).thenReturn(valueOperations);
+
+ adapter.save(transaction);
+
+ verify(valueOperations).set(
+ eq(PREFIX + id),
+ eq(transaction),
+ eq(Duration.ofMinutes(10))
+ );
+ }
+
+ @Test
+ void shouldDeleteTransaction() {
+ UUID id = UUID.randomUUID();
+
+ adapter.delete(id);
+
+ verify(redisTemplate).delete(PREFIX + id);
+ verifyNoInteractions(valueOperations);
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/config/RedisConfigTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/config/RedisConfigTest.java
new file mode 100644
index 0000000000..32093ea366
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/config/RedisConfigTest.java
@@ -0,0 +1,37 @@
+package com.kafka.productor.infrastructure.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+import static org.mockito.Mockito.mock;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import com.kafka.productor.domain.model.Transaction;
+
+class RedisConfigTest {
+
+ private final RedisConfig redisConfig = new RedisConfig();
+
+ @Test
+ void shouldCreateRedisTemplateWithProperConfiguration() {
+
+ RedisConnectionFactory connectionFactory =
+ mock(RedisConnectionFactory.class);
+
+ RedisTemplate template =
+ redisConfig.redisTemplate(connectionFactory);
+
+ assertNotNull(template);
+ assertEquals(connectionFactory, template.getConnectionFactory());
+
+ assertTrue(template.getKeySerializer() instanceof StringRedisSerializer);
+ assertTrue(template.getValueSerializer() instanceof Jackson2JsonRedisSerializer);
+
+ assertTrue(template.getHashKeySerializer() instanceof StringRedisSerializer);
+ assertTrue(template.getHashValueSerializer() instanceof Jackson2JsonRedisSerializer);
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/config/UseCaseConfigTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/config/UseCaseConfigTest.java
new file mode 100644
index 0000000000..9b37ca8ad6
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/config/UseCaseConfigTest.java
@@ -0,0 +1,32 @@
+package com.kafka.productor.infrastructure.config;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+import static org.mockito.Mockito.mock;
+
+import com.kafka.productor.application.usecase.service.create.CreateTransactionService;
+import com.kafka.productor.application.usecase.service.create.CreateTransactionUseCase;
+import com.kafka.productor.domain.port.EventPublisherPort;
+import com.kafka.productor.domain.port.TransactionRepositoryPort;
+
+class UseCaseConfigTest {
+
+ private final UseCaseConfig useCaseConfig = new UseCaseConfig();
+
+ @Test
+ void shouldCreateCreateTransactionUseCaseBean() {
+
+ TransactionRepositoryPort repository =
+ mock(TransactionRepositoryPort.class);
+
+ EventPublisherPort eventPublisher =
+ mock(EventPublisherPort.class);
+
+ CreateTransactionUseCase useCase =
+ useCaseConfig.createTransactionUseCase(repository, eventPublisher);
+
+ assertNotNull(useCase);
+ assertTrue(useCase instanceof CreateTransactionService);
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionRequestTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionRequestTest.java
new file mode 100644
index 0000000000..a247c3654f
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionRequestTest.java
@@ -0,0 +1,54 @@
+package com.kafka.productor.infrastructure.controller.dto;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import org.junit.jupiter.api.Test;
+
+import com.kafka.productor.application.usecase.command.CreateTransactionCommand;
+
+class CreateTransactionRequestTest {
+
+ @Test
+ void shouldMapToCommandCorrectly() {
+ UUID debitId = UUID.randomUUID();
+ UUID creditId = UUID.randomUUID();
+ Long transferType = 1L;
+ double value = 1500.0;
+
+ CreateTransactionRequest request = new CreateTransactionRequest(
+ debitId,
+ creditId,
+ transferType,
+ value
+ );
+
+ CreateTransactionCommand command = request.toCommand();
+
+ assertNotNull(command);
+ assertEquals(debitId, command.accountExternalIdDebit());
+ assertEquals(creditId, command.accountExternalIdCredit());
+ assertEquals(transferType, command.transferTypeId());
+ assertEquals(value, command.value());
+ }
+
+ @Test
+ void shouldAllowNullsAndInvalidsButValidationFailsAtRuntime() {
+ CreateTransactionRequest request = new CreateTransactionRequest(
+ null,
+ null,
+ null,
+ 0
+ );
+
+ CreateTransactionCommand command = request.toCommand();
+
+ assertNotNull(command);
+ assertNull(command.accountExternalIdDebit());
+ assertNull(command.accountExternalIdCredit());
+ assertNull(command.transferTypeId());
+ assertEquals(0, command.value());
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionResponseTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionResponseTest.java
new file mode 100644
index 0000000000..90988f147b
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/controller/dto/CreateTransactionResponseTest.java
@@ -0,0 +1,29 @@
+package com.kafka.productor.infrastructure.controller.dto;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import org.junit.jupiter.api.Test;
+
+class CreateTransactionResponseTest {
+
+ @Test
+ void shouldCreateResponseWithTransactionId() {
+ UUID transactionId = UUID.randomUUID();
+
+ CreateTransactionResponse response = new CreateTransactionResponse(transactionId);
+
+ assertNotNull(response);
+ assertEquals(transactionId, response.transactionExternalId());
+ }
+
+ @Test
+ void shouldHandleNullTransactionId() {
+ CreateTransactionResponse response = new CreateTransactionResponse(null);
+
+ assertNotNull(response);
+ assertNull(response.transactionExternalId());
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/controller/dto/TransactionResponseTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/controller/dto/TransactionResponseTest.java
new file mode 100644
index 0000000000..2f3e0fbcf7
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/controller/dto/TransactionResponseTest.java
@@ -0,0 +1,34 @@
+package com.kafka.productor.infrastructure.controller.dto;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import org.junit.jupiter.api.Test;
+
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.domain.model.TransactionStatus;
+import com.kafka.productor.domain.model.TransactionType;
+
+public class TransactionResponseTest {
+ @Test
+ void shouldMapTransactionToResponse() {
+ UUID transactionId = UUID.randomUUID();
+ UUID accountDebit = UUID.randomUUID();
+ UUID accountCredit = UUID.randomUUID();
+ TransactionType type = new TransactionType(1L, "TRANSFER");
+ Transaction transaction = new Transaction(transactionId, accountDebit, accountCredit, type, 1000.0);
+ transaction.setStatus(TransactionStatus.APPROVED);
+ transaction.setCreatedAt(LocalDateTime.now());
+
+ TransactionResponse response = TransactionResponse.from(transaction);
+
+ assertNotNull(response);
+ assertEquals(transactionId, response.transactionExternalId());
+ assertEquals("TRANSFER", response.transactionType().name());
+ assertEquals("APPROVED", response.transactionStatus().name());
+ assertEquals(1000.0, response.value());
+ assertEquals(transaction.getCreatedAt(), response.createdAt());
+ }
+}
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/mapper/TransactionMapperImplTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/mapper/TransactionMapperImplTest.java
new file mode 100644
index 0000000000..d982505564
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/mapper/TransactionMapperImplTest.java
@@ -0,0 +1,153 @@
+package com.kafka.productor.infrastructure.mapper;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import org.junit.jupiter.api.Test;
+
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.domain.model.TransactionStatus;
+import com.kafka.productor.domain.model.TransactionType;
+import com.kafka.productor.infrastructure.persistence.entity.TransactionEntity;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+class TransactionMapperImplTest {
+
+ private final TransactionMapperImpl mapper = new TransactionMapperImpl();
+
+ @Test
+ void shouldReturnNullWhenTransactionIsNull() {
+ assertNull(mapper.toEntity(null));
+ }
+
+ @Test
+ void shouldMapTransactionWithAllFields() {
+
+ UUID id = UUID.randomUUID();
+ UUID debit = UUID.randomUUID();
+ UUID credit = UUID.randomUUID();
+ double value = 10.0;
+ LocalDateTime createdAt = LocalDateTime.now();
+
+ TransactionType type = new TransactionType(1L, "TRANSFER");
+
+ Transaction transaction = new Transaction(
+ id,
+ debit,
+ credit,
+ type,
+ value);
+
+ transaction.setStatus(TransactionStatus.APPROVED);
+ transaction.setCreatedAt(createdAt);
+
+ TransactionEntity entity = mapper.toEntity(transaction);
+
+ assertNotNull(entity);
+ assertEquals(id, entity.getTransactionExternalId());
+ assertEquals(debit, entity.getAccountExternalIdDebit());
+ assertEquals(credit, entity.getAccountExternalIdCredit());
+ assertEquals(1L, entity.getTransactionTypeId());
+ assertEquals("TRANSFER", entity.getTransactionTypeName());
+ assertEquals("APPROVED", entity.getStatus());
+ assertEquals(value, entity.getValue());
+ assertEquals(createdAt, entity.getCreatedAt());
+ }
+
+ @Test
+ void shouldMapTransactionWhenTransactionTypeIsNull() {
+
+ Transaction transaction = new Transaction(
+ UUID.randomUUID(),
+ UUID.randomUUID(),
+ UUID.randomUUID(),
+ null,
+ 10.0);
+
+ transaction.setStatus(TransactionStatus.REJECTED);
+
+ TransactionEntity entity = mapper.toEntity(transaction);
+
+ assertNull(entity.getTransactionTypeId());
+ assertNull(entity.getTransactionTypeName());
+ assertEquals("REJECTED", entity.getStatus());
+ }
+
+ @Test
+ void shouldMapTransactionWhenStatusIsNull() {
+
+ TransactionType type = new TransactionType(1L, "TRANSFER");
+
+ Transaction transaction = new Transaction(
+ UUID.randomUUID(),
+ UUID.randomUUID(),
+ UUID.randomUUID(),
+ type,
+ 1001.0);
+
+ transaction.setStatus(null);
+
+ TransactionEntity entity = mapper.toEntity(transaction);
+
+ assertEquals(1L, entity.getTransactionTypeId());
+ assertEquals("TRANSFER", entity.getTransactionTypeName());
+ assertNull(entity.getStatus());
+ }
+
+ @Test
+ void shouldReturnNullWhenEntityIsNull() {
+ assertNull(mapper.toDomain(null));
+ }
+
+ @Test
+ void shouldMapEntityWithAllFields() {
+
+ UUID id = UUID.randomUUID();
+ UUID debit = UUID.randomUUID();
+ UUID credit = UUID.randomUUID();
+ double value = 10.0;
+
+ TransactionEntity entity = new TransactionEntity();
+ entity.setTransactionExternalId(id);
+ entity.setAccountExternalIdDebit(debit);
+ entity.setAccountExternalIdCredit(credit);
+ entity.setTransactionTypeId(1L);
+ entity.setTransactionTypeName("TRANSFER");
+ entity.setStatus("REJECTED");
+ entity.setValue(value);
+
+ Transaction transaction = mapper.toDomain(entity);
+
+ assertNotNull(transaction);
+ assertEquals(id, transaction.getTransactionExternalId());
+ assertEquals(debit, transaction.getAccountExternalIdDebit());
+ assertEquals(credit, transaction.getAccountExternalIdCredit());
+ assertEquals("TRANSFER", transaction.getTransactionType().getName());
+ assertEquals(TransactionStatus.REJECTED, transaction.getStatus());
+ assertEquals(value, transaction.getValue());
+ }
+
+ @Test
+ void shouldMapEntityWhenStatusIsNull() {
+
+ TransactionEntity entity = new TransactionEntity();
+ entity.setTransactionExternalId(UUID.randomUUID());
+ entity.setAccountExternalIdDebit(UUID.randomUUID());
+ entity.setAccountExternalIdCredit(UUID.randomUUID());
+ entity.setTransactionTypeId(1L);
+ entity.setTransactionTypeName("TRANSFER");
+ entity.setStatus("APPROVED");
+ entity.setValue(1001.0);
+
+ Transaction transaction = mapper.toDomain(entity);
+
+ assertNotNull(transaction);
+
+ assertEquals(TransactionStatus.APPROVED, transaction.getStatus());
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/TransactionStatusUpdatedEventTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/TransactionStatusUpdatedEventTest.java
new file mode 100644
index 0000000000..4f041d6ad9
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/TransactionStatusUpdatedEventTest.java
@@ -0,0 +1,24 @@
+package com.kafka.productor.infrastructure.messaging;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+
+import com.kafka.productor.infrastructure.messaging.event.TransactionStatusUpdatedEvent;
+
+class TransactionStatusUpdatedEventTest {
+
+ @Test
+ void shouldCreateRecordCorrectly() {
+
+ UUID id = UUID.randomUUID();
+ String status = "COMPLETED";
+
+ TransactionStatusUpdatedEvent event =
+ new TransactionStatusUpdatedEvent(id, status);
+
+ assertEquals(id, event.transactionExternalId());
+ assertEquals(status, event.status());
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionCreatedEventTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionCreatedEventTest.java
new file mode 100644
index 0000000000..38cd095620
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionCreatedEventTest.java
@@ -0,0 +1,30 @@
+package com.kafka.productor.infrastructure.messaging.kafka;
+
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.Test;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.kafka.productor.domain.model.Transaction;
+
+class TransactionCreatedEventTest {
+
+ @Test
+ void shouldCreateEventFromTransaction() {
+
+ UUID id = UUID.randomUUID();
+ double value = 150.0;
+
+ Transaction transaction = mock(Transaction.class);
+ when(transaction.getTransactionExternalId()).thenReturn(id);
+ when(transaction.getValue()).thenReturn(value);
+
+ TransactionCreatedEvent event =
+ TransactionCreatedEvent.from(transaction);
+
+ assertEquals(id, event.transactionExternalId());
+ assertEquals(value, event.value());
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaListenerTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaListenerTest.java
new file mode 100644
index 0000000000..4080252bf7
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaListenerTest.java
@@ -0,0 +1,38 @@
+package com.kafka.productor.infrastructure.messaging.kafka;
+
+import java.util.UUID;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import static org.mockito.Mockito.verify;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.kafka.productor.application.usecase.service.update.UpdateTransactionStatusUseCase;
+import com.kafka.productor.infrastructure.messaging.event.TransactionStatusUpdatedEvent;
+
+@ExtendWith(MockitoExtension.class)
+class TransactionKafkaListenerTest {
+
+ @Mock
+ private UpdateTransactionStatusUseCase updateTransactionStatusUseCase;
+
+ @InjectMocks
+ private TransactionKafkaListener listener;
+
+ @Test
+ void shouldExecuteUseCaseWhenEventReceived() {
+
+ UUID id = UUID.randomUUID();
+ String status = "APPROVED";
+
+ TransactionStatusUpdatedEvent event =
+ new TransactionStatusUpdatedEvent(id, status);
+
+ listener.publishTransactionStatusUpdated(event);
+
+ verify(updateTransactionStatusUseCase)
+ .execute(id, status);
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaProducerTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaProducerTest.java
new file mode 100644
index 0000000000..78d12c1b5f
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/messaging/kafka/TransactionKafkaProducerTest.java
@@ -0,0 +1,53 @@
+package com.kafka.productor.infrastructure.messaging.kafka;
+
+import java.lang.reflect.Field;
+import java.util.UUID;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.eq;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.kafka.core.KafkaTemplate;
+
+import com.kafka.productor.domain.model.Transaction;
+
+@ExtendWith(MockitoExtension.class)
+class TransactionKafkaProducerTest {
+
+ @Mock
+ private KafkaTemplate kafkaTemplate;
+
+ @InjectMocks
+ private TransactionKafkaProducer producer;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ Field field = TransactionKafkaProducer.class
+ .getDeclaredField("topicCreated");
+ field.setAccessible(true);
+ field.set(producer, "transaction-created-topic");
+ }
+
+ @Test
+ void shouldPublishTransactionCreatedEvent() {
+
+ UUID id = UUID.randomUUID();
+
+ Transaction transaction = mock(Transaction.class);
+ when(transaction.getTransactionExternalId()).thenReturn(id);
+
+ producer.publishTransactionCreated(transaction);
+
+ verify(kafkaTemplate).send(
+ eq("transaction-created-topic"),
+ eq(id.toString()),
+ eq(transaction)
+ );
+ }
+}
\ No newline at end of file
diff --git a/yape-productor/src/test/java/com/kafka/productor/infrastructure/persistence/adapter/TransactionRepositoryAdapterTest.java b/yape-productor/src/test/java/com/kafka/productor/infrastructure/persistence/adapter/TransactionRepositoryAdapterTest.java
new file mode 100644
index 0000000000..702903143d
--- /dev/null
+++ b/yape-productor/src/test/java/com/kafka/productor/infrastructure/persistence/adapter/TransactionRepositoryAdapterTest.java
@@ -0,0 +1,123 @@
+package com.kafka.productor.infrastructure.persistence.adapter;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import com.kafka.productor.domain.model.Transaction;
+import com.kafka.productor.infrastructure.mapper.TransactionMapperInterface;
+import com.kafka.productor.infrastructure.persistence.entity.TransactionEntity;
+import com.kafka.productor.infrastructure.persistence.repository.JpaTransactionRepository;
+
+@ExtendWith(MockitoExtension.class)
+class TransactionRepositoryAdapterTest {
+
+ @Mock
+ private JpaTransactionRepository repository;
+
+ @Mock
+ private TransactionMapperInterface mapper;
+
+ @InjectMocks
+ private TransactionRepositoryAdapter adapter;
+
+ @Test
+ void shouldSaveTransaction() {
+
+ Transaction transaction = mock(Transaction.class);
+ TransactionEntity entity = new TransactionEntity();
+
+ when(mapper.toEntity(transaction)).thenReturn(entity);
+ when(repository.save(entity)).thenReturn(entity);
+
+ Transaction result = adapter.save(transaction);
+
+ verify(mapper).toEntity(transaction);
+ verify(repository).save(entity);
+ assertEquals(transaction, result);
+ }
+
+ @Test
+ void shouldFindTransactionByExternalId() {
+
+ UUID id = UUID.randomUUID();
+ TransactionEntity entity = new TransactionEntity();
+ Transaction transaction = mock(Transaction.class);
+
+ when(repository.findByTransactionExternalId(id))
+ .thenReturn(Optional.of(entity));
+
+ when(mapper.toDomain(entity)).thenReturn(transaction);
+
+ Optional result = adapter.findByTransactionExternalId(id);
+
+ assertTrue(result.isPresent());
+ assertEquals(transaction, result.get());
+
+ verify(repository).findByTransactionExternalId(id);
+ verify(mapper).toDomain(entity);
+ }
+
+ @Test
+ void shouldReturnEmptyWhenTransactionNotFound() {
+
+ UUID id = UUID.randomUUID();
+
+ when(repository.findByTransactionExternalId(id))
+ .thenReturn(Optional.empty());
+
+ Optional result = adapter.findByTransactionExternalId(id);
+
+ assertTrue(result.isEmpty());
+ verify(repository).findByTransactionExternalId(id);
+ verifyNoInteractions(mapper);
+ }
+
+ @Test
+ void shouldUpdateStatus() {
+
+ UUID id = UUID.randomUUID();
+ String newStatus = "APPROVED";
+
+ TransactionEntity entity = new TransactionEntity();
+
+ when(repository.findByTransactionExternalId(id))
+ .thenReturn(Optional.of(entity));
+
+ adapter.updateStatus(id, newStatus);
+
+ assertEquals(newStatus, entity.getStatus());
+ verify(repository).save(entity);
+ }
+
+ @Test
+ void shouldThrowExceptionWhenUpdatingStatusAndTransactionNotFound() {
+
+ UUID id = UUID.randomUUID();
+
+ when(repository.findByTransactionExternalId(id))
+ .thenReturn(Optional.empty());
+
+ assertThrows(
+ RuntimeException.class,
+ () -> adapter.updateStatus(id, "REJECTED")
+ );
+
+ verify(repository).findByTransactionExternalId(id);
+ verify(repository, never()).save(any());
+ }
+}
\ No newline at end of file