From c0c2d5b35778ca9c00a1704688306ff9405ae4e8 Mon Sep 17 00:00:00 2001 From: Goswin Vasquez Date: Tue, 24 Feb 2026 01:37:31 -0500 Subject: [PATCH] feat: implement transaction anti-fraud service with Hexagonal Architecture --- .DS_Store | Bin 0 -> 6148 bytes .idea/.gitignore | 8 + .idea/compiler.xml | 22 ++ .idea/copilot.data.migration.ask2agent.xml | 6 + .idea/encodings.xml | 7 + .idea/jarRepositories.xml | 35 +++ .idea/misc.xml | 12 + .idea/vcs.xml | 6 + HELP.md | 27 ++ mvnw | 295 ++++++++++++++++++ mvnw.cmd | 189 +++++++++++ pom.xml | 97 ++++++ .../java/com/yape/YapePruebaApplication.java | 13 + .../port/in/CreateTransactionUseCase.java | 7 + .../port/in/GetTransactionUseCase.java | 9 + .../out/TransactionEventPublisherPort.java | 7 + .../port/out/TransactionRepositoryPort.java | 11 + .../service/TransactionService.java | 48 +++ .../TransactionNotFoundException.java | 7 + .../transaction/domain/model/Transaction.java | 24 ++ .../domain/model/TransactionStatus.java | 7 + .../in/kafka/TransactionKafkaConsumer.java | 35 +++ .../in/rest/GlobalExceptionHandler.java | 16 + .../in/rest/TransactionController.java | 33 ++ .../in/rest/dto/TransactionResponse.java | 45 +++ .../TransactionPersistenceAdapter.java | 54 ++++ .../database/entity/TransactionEntity.java | 46 +++ .../repository/TransactionRepository.java | 11 + .../out/kafka/TransactionKafkaProducer.java | 23 ++ .../adapter/out/kafka/package-info.java | 1 + .../infrastructure/config/package-info.java | 1 + src/main/resources/application.properties | 19 ++ .../com/yape/YapePruebaApplicationTests.java | 13 + .../service/TransactionServiceTest.java | 100 ++++++ .../kafka/TransactionKafkaConsumerTest.java | 65 ++++ .../in/rest/TransactionControllerTest.java | 97 ++++++ .../TransactionPersistenceAdapterTest.java | 102 ++++++ target/classes/application.properties | 19 ++ .../com/yape/YapePruebaApplication.class | Bin 0 -> 735 bytes .../port/in/CreateTransactionUseCase.class | Bin 0 -> 341 bytes .../port/in/GetTransactionUseCase.class | Bin 0 -> 311 bytes .../out/TransactionEventPublisherPort.class | Bin 0 -> 314 bytes .../port/out/TransactionRepositoryPort.class | Bin 0 -> 517 bytes .../service/TransactionService.class | Bin 0 -> 4096 bytes .../TransactionNotFoundException.class | Bin 0 -> 489 bytes .../Transaction$TransactionBuilder.class | Bin 0 -> 3436 bytes .../domain/model/Transaction.class | Bin 0 -> 6226 bytes .../domain/model/TransactionStatus.class | Bin 0 -> 1324 bytes .../in/kafka/TransactionKafkaConsumer.class | Bin 0 -> 2426 bytes .../in/rest/GlobalExceptionHandler.class | Bin 0 -> 1418 bytes .../in/rest/TransactionController.class | Bin 0 -> 2872 bytes .../TransactionPersistenceAdapter.class | Bin 0 -> 5463 bytes ...ctionEntity$TransactionEntityBuilder.class | Bin 0 -> 3826 bytes .../database/entity/TransactionEntity.class | Bin 0 -> 6979 bytes .../repository/TransactionRepository.class | Bin 0 -> 535 bytes .../out/kafka/TransactionKafkaProducer.class | Bin 0 -> 2089 bytes 56 files changed, 1517 insertions(+) create mode 100644 .DS_Store create mode 100644 .idea/.gitignore create mode 100644 .idea/compiler.xml create mode 100644 .idea/copilot.data.migration.ask2agent.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 HELP.md create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml create mode 100644 src/main/java/com/yape/YapePruebaApplication.java create mode 100644 src/main/java/com/yape/transaction/application/port/in/CreateTransactionUseCase.java create mode 100644 src/main/java/com/yape/transaction/application/port/in/GetTransactionUseCase.java create mode 100644 src/main/java/com/yape/transaction/application/port/out/TransactionEventPublisherPort.java create mode 100644 src/main/java/com/yape/transaction/application/port/out/TransactionRepositoryPort.java create mode 100644 src/main/java/com/yape/transaction/application/service/TransactionService.java create mode 100644 src/main/java/com/yape/transaction/domain/exception/TransactionNotFoundException.java create mode 100644 src/main/java/com/yape/transaction/domain/model/Transaction.java create mode 100644 src/main/java/com/yape/transaction/domain/model/TransactionStatus.java create mode 100644 src/main/java/com/yape/transaction/infrastructure/adapter/in/kafka/TransactionKafkaConsumer.java create mode 100644 src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/GlobalExceptionHandler.java create mode 100644 src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/TransactionController.java create mode 100644 src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/dto/TransactionResponse.java create mode 100644 src/main/java/com/yape/transaction/infrastructure/adapter/out/database/TransactionPersistenceAdapter.java create mode 100644 src/main/java/com/yape/transaction/infrastructure/adapter/out/database/entity/TransactionEntity.java create mode 100644 src/main/java/com/yape/transaction/infrastructure/adapter/out/database/repository/TransactionRepository.java create mode 100644 src/main/java/com/yape/transaction/infrastructure/adapter/out/kafka/TransactionKafkaProducer.java create mode 100644 src/main/java/com/yape/transaction/infrastructure/adapter/out/kafka/package-info.java create mode 100644 src/main/java/com/yape/transaction/infrastructure/config/package-info.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/com/yape/YapePruebaApplicationTests.java create mode 100644 src/test/java/com/yape/transaction/application/service/TransactionServiceTest.java create mode 100644 src/test/java/com/yape/transaction/infrastructure/adapter/in/kafka/TransactionKafkaConsumerTest.java create mode 100644 src/test/java/com/yape/transaction/infrastructure/adapter/in/rest/TransactionControllerTest.java create mode 100644 src/test/java/com/yape/transaction/infrastructure/adapter/out/database/TransactionPersistenceAdapterTest.java create mode 100644 target/classes/application.properties create mode 100644 target/classes/com/yape/YapePruebaApplication.class create mode 100644 target/classes/com/yape/transaction/application/port/in/CreateTransactionUseCase.class create mode 100644 target/classes/com/yape/transaction/application/port/in/GetTransactionUseCase.class create mode 100644 target/classes/com/yape/transaction/application/port/out/TransactionEventPublisherPort.class create mode 100644 target/classes/com/yape/transaction/application/port/out/TransactionRepositoryPort.class create mode 100644 target/classes/com/yape/transaction/application/service/TransactionService.class create mode 100644 target/classes/com/yape/transaction/domain/exception/TransactionNotFoundException.class create mode 100644 target/classes/com/yape/transaction/domain/model/Transaction$TransactionBuilder.class create mode 100644 target/classes/com/yape/transaction/domain/model/Transaction.class create mode 100644 target/classes/com/yape/transaction/domain/model/TransactionStatus.class create mode 100644 target/classes/com/yape/transaction/infrastructure/adapter/in/kafka/TransactionKafkaConsumer.class create mode 100644 target/classes/com/yape/transaction/infrastructure/adapter/in/rest/GlobalExceptionHandler.class create mode 100644 target/classes/com/yape/transaction/infrastructure/adapter/in/rest/TransactionController.class create mode 100644 target/classes/com/yape/transaction/infrastructure/adapter/out/database/TransactionPersistenceAdapter.class create mode 100644 target/classes/com/yape/transaction/infrastructure/adapter/out/database/entity/TransactionEntity$TransactionEntityBuilder.class create mode 100644 target/classes/com/yape/transaction/infrastructure/adapter/out/database/entity/TransactionEntity.class create mode 100644 target/classes/com/yape/transaction/infrastructure/adapter/out/database/repository/TransactionRepository.class create mode 100644 target/classes/com/yape/transaction/infrastructure/adapter/out/kafka/TransactionKafkaProducer.class diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..81229e8d26382bd3469cb3d9bb2de3f1954844ac GIT binary patch literal 6148 zcmeH~Jqp4=5QS&dB4Cr!avKle4VIuM@B*S@B?yZB9^E%TjnP_yyn&f-XEsBUS7b9H zqQmpN5$Q#wgBxXSVPuMYE)TiO>2iLYjtb*@FJ*K=2U&T%hcRwa*e@u>x3=Er<$CqZN!+^)bZi z-VT<$t|nVB+C_8t(7dzS6a&*}7cEF&S{)2jfC`Khm`C2*`M-mIoBu~GOsN1B_%j7` zvE6S6yi}g8AFpTiLso6w;GkcQ@b(jc#E#+>+ztE17GO=bASy8a2)GOkRN$uyya2`( B5q1Co literal 0 HcmV?d00001 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..13566b81b0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000000..245588ac78 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000000..1f2ea11e7f --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000000..0e1c064652 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000000..90d3d281ad --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..0abcc97ca2 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..35eb1ddfbb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/HELP.md b/HELP.md new file mode 100644 index 0000000000..49d8804cc7 --- /dev/null +++ b/HELP.md @@ -0,0 +1,27 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.11/maven-plugin) +* [Create an OCI image](https://docs.spring.io/spring-boot/3.5.11/maven-plugin/build-image.html) +* [Spring Web](https://docs.spring.io/spring-boot/3.5.11/reference/web/servlet.html) +* [Spring Data JPA](https://docs.spring.io/spring-boot/3.5.11/reference/data/sql.html#data.sql.jpa-and-spring-data) +* [Spring for Apache Kafka](https://docs.spring.io/spring-boot/3.5.11/reference/messaging/kafka.html) + +### Guides +The following guides illustrate how to use some features concretely: + +* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) +* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) +* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) +* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) + +### Maven Parent overrides + +Due to Maven's design, elements are inherited from the parent POM to the project POM. +While most of the inheritance is fine, it also inherits unwanted elements like `` and `` from the parent. +To prevent this, the project POM contains empty overrides for these elements. +If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides. + diff --git a/mvnw b/mvnw new file mode 100755 index 0000000000..bd8896bf22 --- /dev/null +++ b/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/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000000..92450f9327 --- /dev/null +++ b/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/pom.xml b/pom.xml new file mode 100644 index 0000000000..86d7b13e5c --- /dev/null +++ b/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.11 + + + com.yape + yapePrueba + 0.0.1-SNAPSHOT + yapePrueba + Prueba de Transacciones Yape + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.kafka + spring-kafka + + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.kafka + spring-kafka-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/yape/YapePruebaApplication.java b/src/main/java/com/yape/YapePruebaApplication.java new file mode 100644 index 0000000000..271a334052 --- /dev/null +++ b/src/main/java/com/yape/YapePruebaApplication.java @@ -0,0 +1,13 @@ +package com.yape; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class YapePruebaApplication { + + public static void main(String[] args) { + SpringApplication.run(YapePruebaApplication.class, args); + } + +} diff --git a/src/main/java/com/yape/transaction/application/port/in/CreateTransactionUseCase.java b/src/main/java/com/yape/transaction/application/port/in/CreateTransactionUseCase.java new file mode 100644 index 0000000000..4589f37c92 --- /dev/null +++ b/src/main/java/com/yape/transaction/application/port/in/CreateTransactionUseCase.java @@ -0,0 +1,7 @@ +package com.yape.transaction.application.port.in; + +import com.yape.transaction.domain.model.Transaction; + +public interface CreateTransactionUseCase { + Transaction createTransaction(Transaction transaction); +} diff --git a/src/main/java/com/yape/transaction/application/port/in/GetTransactionUseCase.java b/src/main/java/com/yape/transaction/application/port/in/GetTransactionUseCase.java new file mode 100644 index 0000000000..4c2e8d2bf1 --- /dev/null +++ b/src/main/java/com/yape/transaction/application/port/in/GetTransactionUseCase.java @@ -0,0 +1,9 @@ +package com.yape.transaction.application.port.in; + +import com.yape.transaction.domain.model.Transaction; + +import java.util.UUID; + +public interface GetTransactionUseCase { + Transaction getTransaction(UUID transactionExternalId); +} diff --git a/src/main/java/com/yape/transaction/application/port/out/TransactionEventPublisherPort.java b/src/main/java/com/yape/transaction/application/port/out/TransactionEventPublisherPort.java new file mode 100644 index 0000000000..2e72acdb03 --- /dev/null +++ b/src/main/java/com/yape/transaction/application/port/out/TransactionEventPublisherPort.java @@ -0,0 +1,7 @@ +package com.yape.transaction.application.port.out; + +import com.yape.transaction.domain.model.Transaction; + +public interface TransactionEventPublisherPort { + void publishTransactionCreated(Transaction transaction); +} diff --git a/src/main/java/com/yape/transaction/application/port/out/TransactionRepositoryPort.java b/src/main/java/com/yape/transaction/application/port/out/TransactionRepositoryPort.java new file mode 100644 index 0000000000..e2c9f0f070 --- /dev/null +++ b/src/main/java/com/yape/transaction/application/port/out/TransactionRepositoryPort.java @@ -0,0 +1,11 @@ +package com.yape.transaction.application.port.out; + +import com.yape.transaction.domain.model.Transaction; + +import java.util.Optional; +import java.util.UUID; + +public interface TransactionRepositoryPort { + Transaction save(Transaction transaction); + Optional findById(UUID id); +} diff --git a/src/main/java/com/yape/transaction/application/service/TransactionService.java b/src/main/java/com/yape/transaction/application/service/TransactionService.java new file mode 100644 index 0000000000..134afcd6d8 --- /dev/null +++ b/src/main/java/com/yape/transaction/application/service/TransactionService.java @@ -0,0 +1,48 @@ +package com.yape.transaction.application.service; + +import com.yape.transaction.application.port.in.CreateTransactionUseCase; +import com.yape.transaction.application.port.in.GetTransactionUseCase; +import com.yape.transaction.application.port.out.TransactionEventPublisherPort; +import com.yape.transaction.application.port.out.TransactionRepositoryPort; +import com.yape.transaction.domain.exception.TransactionNotFoundException; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.domain.model.TransactionStatus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TransactionService implements CreateTransactionUseCase, GetTransactionUseCase { + + private final TransactionRepositoryPort transactionRepository; + private final TransactionEventPublisherPort transactionEventPublisher; + + @Override + @Transactional + public Transaction createTransaction(Transaction transaction) { + log.info("Creating transaction for account: {}", transaction.getAccountExternalIdDebit()); + + // Initial status is PENDING + transaction.setStatus(TransactionStatus.PENDING); + + // Save to database + Transaction savedTransaction = transactionRepository.save(transaction); + + // Publish event to Kafka + transactionEventPublisher.publishTransactionCreated(savedTransaction); + + return savedTransaction; + } + + @Override + @Transactional(readOnly = true) + public Transaction getTransaction(UUID transactionExternalId) { + return transactionRepository.findById(transactionExternalId) + .orElseThrow(() -> new TransactionNotFoundException("Transaction not found with id: " + transactionExternalId)); + } +} diff --git a/src/main/java/com/yape/transaction/domain/exception/TransactionNotFoundException.java b/src/main/java/com/yape/transaction/domain/exception/TransactionNotFoundException.java new file mode 100644 index 0000000000..c5603678c2 --- /dev/null +++ b/src/main/java/com/yape/transaction/domain/exception/TransactionNotFoundException.java @@ -0,0 +1,7 @@ +package com.yape.transaction.domain.exception; + +public class TransactionNotFoundException extends RuntimeException { + public TransactionNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/com/yape/transaction/domain/model/Transaction.java b/src/main/java/com/yape/transaction/domain/model/Transaction.java new file mode 100644 index 0000000000..68f6c2e924 --- /dev/null +++ b/src/main/java/com/yape/transaction/domain/model/Transaction.java @@ -0,0 +1,24 @@ +package com.yape.transaction.domain.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Transaction { + private UUID transactionExternalId; + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private Integer transferTypeId; + private BigDecimal value; + private TransactionStatus status; + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/yape/transaction/domain/model/TransactionStatus.java b/src/main/java/com/yape/transaction/domain/model/TransactionStatus.java new file mode 100644 index 0000000000..4bd38b0060 --- /dev/null +++ b/src/main/java/com/yape/transaction/domain/model/TransactionStatus.java @@ -0,0 +1,7 @@ +package com.yape.transaction.domain.model; + +public enum TransactionStatus { + PENDING, + APPROVED, + REJECTED +} diff --git a/src/main/java/com/yape/transaction/infrastructure/adapter/in/kafka/TransactionKafkaConsumer.java b/src/main/java/com/yape/transaction/infrastructure/adapter/in/kafka/TransactionKafkaConsumer.java new file mode 100644 index 0000000000..d2f9a64792 --- /dev/null +++ b/src/main/java/com/yape/transaction/infrastructure/adapter/in/kafka/TransactionKafkaConsumer.java @@ -0,0 +1,35 @@ +package com.yape.transaction.infrastructure.adapter.in.kafka; + +import com.yape.transaction.application.port.out.TransactionRepositoryPort; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.domain.model.TransactionStatus; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TransactionKafkaConsumer { + + private final TransactionRepositoryPort transactionRepository; + + @KafkaListener(topics = "transaction-created", groupId = "transaction-group") + public void consumeTransactionCreated(Transaction transaction) { + log.info("Received transaction created event: {}", transaction.getTransactionExternalId()); + + // Anti-fraud logic (simplified) + if (transaction.getValue().compareTo(new BigDecimal("1000")) > 0) { + transaction.setStatus(TransactionStatus.REJECTED); + } else { + transaction.setStatus(TransactionStatus.APPROVED); + } + + // Update transaction status in database + transactionRepository.save(transaction); + log.info("Transaction status updated to: {}", transaction.getStatus()); + } +} diff --git a/src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/GlobalExceptionHandler.java b/src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/GlobalExceptionHandler.java new file mode 100644 index 0000000000..71004bb617 --- /dev/null +++ b/src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/GlobalExceptionHandler.java @@ -0,0 +1,16 @@ +package com.yape.transaction.infrastructure.adapter.in.rest; + +import com.yape.transaction.domain.exception.TransactionNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(TransactionNotFoundException.class) + public ResponseEntity handleTransactionNotFoundException(TransactionNotFoundException ex) { + return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND); + } +} diff --git a/src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/TransactionController.java b/src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/TransactionController.java new file mode 100644 index 0000000000..ecaa56068f --- /dev/null +++ b/src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/TransactionController.java @@ -0,0 +1,33 @@ +package com.yape.transaction.infrastructure.adapter.in.rest; + +import com.yape.transaction.application.port.in.CreateTransactionUseCase; +import com.yape.transaction.application.port.in.GetTransactionUseCase; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.infrastructure.adapter.in.rest.dto.TransactionResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/transactions") +@RequiredArgsConstructor +public class TransactionController { + + private final CreateTransactionUseCase createTransactionUseCase; + private final GetTransactionUseCase getTransactionUseCase; + + @PostMapping + public ResponseEntity createTransaction(@RequestBody Transaction transaction) { + Transaction createdTransaction = createTransactionUseCase.createTransaction(transaction); + return new ResponseEntity<>(TransactionResponse.fromDomain(createdTransaction), HttpStatus.CREATED); + } + + @GetMapping("/{transactionExternalId}") + public ResponseEntity getTransaction(@PathVariable UUID transactionExternalId) { + Transaction transaction = getTransactionUseCase.getTransaction(transactionExternalId); + return ResponseEntity.ok(TransactionResponse.fromDomain(transaction)); + } +} diff --git a/src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/dto/TransactionResponse.java b/src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/dto/TransactionResponse.java new file mode 100644 index 0000000000..fa9c9fafb9 --- /dev/null +++ b/src/main/java/com/yape/transaction/infrastructure/adapter/in/rest/dto/TransactionResponse.java @@ -0,0 +1,45 @@ +package com.yape.transaction.infrastructure.adapter.in.rest.dto; + +import com.yape.transaction.domain.model.Transaction; +import lombok.Builder; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Data +@Builder +public class TransactionResponse { + private UUID transactionExternalId; + private TransactionTypeDto transactionType; + private TransactionStatusDto transactionStatus; + private BigDecimal value; + private LocalDateTime createdAt; + + @Data + @Builder + public static class TransactionTypeDto { + private String name; + } + + @Data + @Builder + public static class TransactionStatusDto { + private String name; + } + + public static TransactionResponse fromDomain(Transaction transaction) { + return TransactionResponse.builder() + .transactionExternalId(transaction.getTransactionExternalId()) + .transactionType(TransactionTypeDto.builder() + .name(transaction.getTransferTypeId() == 1 ? "TRANSFER" : "UNKNOWN") + .build()) + .transactionStatus(TransactionStatusDto.builder() + .name(transaction.getStatus().name()) + .build()) + .value(transaction.getValue()) + .createdAt(transaction.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/yape/transaction/infrastructure/adapter/out/database/TransactionPersistenceAdapter.java b/src/main/java/com/yape/transaction/infrastructure/adapter/out/database/TransactionPersistenceAdapter.java new file mode 100644 index 0000000000..7da979fb49 --- /dev/null +++ b/src/main/java/com/yape/transaction/infrastructure/adapter/out/database/TransactionPersistenceAdapter.java @@ -0,0 +1,54 @@ +package com.yape.transaction.infrastructure.adapter.out.database; + +import com.yape.transaction.application.port.out.TransactionRepositoryPort; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.infrastructure.adapter.out.database.entity.TransactionEntity; +import com.yape.transaction.infrastructure.adapter.out.database.repository.TransactionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class TransactionPersistenceAdapter implements TransactionRepositoryPort { + + private final TransactionRepository transactionRepository; + + @Override + public Transaction save(Transaction transaction) { + TransactionEntity entity = TransactionEntity.builder() + .accountExternalIdDebit(transaction.getAccountExternalIdDebit()) + .accountExternalIdCredit(transaction.getAccountExternalIdCredit()) + .transferTypeId(transaction.getTransferTypeId()) + .value(transaction.getValue()) + .status(transaction.getStatus()) + .build(); + + if (transaction.getTransactionExternalId() != null) { + entity.setTransactionExternalId(transaction.getTransactionExternalId()); + } + + TransactionEntity savedEntity = transactionRepository.save(entity); + return mapToDomain(savedEntity); + } + + @Override + public Optional findById(UUID id) { + return transactionRepository.findById(id) + .map(this::mapToDomain); + } + + private Transaction mapToDomain(TransactionEntity entity) { + return Transaction.builder() + .transactionExternalId(entity.getTransactionExternalId()) + .accountExternalIdDebit(entity.getAccountExternalIdDebit()) + .accountExternalIdCredit(entity.getAccountExternalIdCredit()) + .transferTypeId(entity.getTransferTypeId()) + .value(entity.getValue()) + .status(entity.getStatus()) + .createdAt(entity.getCreatedAt()) + .build(); + } +} diff --git a/src/main/java/com/yape/transaction/infrastructure/adapter/out/database/entity/TransactionEntity.java b/src/main/java/com/yape/transaction/infrastructure/adapter/out/database/entity/TransactionEntity.java new file mode 100644 index 0000000000..9dc7befbfd --- /dev/null +++ b/src/main/java/com/yape/transaction/infrastructure/adapter/out/database/entity/TransactionEntity.java @@ -0,0 +1,46 @@ +package com.yape.transaction.infrastructure.adapter.out.database.entity; + +import com.yape.transaction.domain.model.TransactionStatus; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "transactions") +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TransactionEntity { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + private UUID transactionExternalId; + + @Column(nullable = false) + private UUID accountExternalIdDebit; + + @Column(nullable = false) + private UUID accountExternalIdCredit; + + @Column(nullable = false) + private Integer transferTypeId; + + @Column(nullable = false) + private BigDecimal value; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private TransactionStatus status; + + @CreationTimestamp + @Column(updatable = false) + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/yape/transaction/infrastructure/adapter/out/database/repository/TransactionRepository.java b/src/main/java/com/yape/transaction/infrastructure/adapter/out/database/repository/TransactionRepository.java new file mode 100644 index 0000000000..2324278b1a --- /dev/null +++ b/src/main/java/com/yape/transaction/infrastructure/adapter/out/database/repository/TransactionRepository.java @@ -0,0 +1,11 @@ +package com.yape.transaction.infrastructure.adapter.out.database.repository; + +import com.yape.transaction.infrastructure.adapter.out.database.entity.TransactionEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.UUID; + +@Repository +public interface TransactionRepository extends JpaRepository { +} diff --git a/src/main/java/com/yape/transaction/infrastructure/adapter/out/kafka/TransactionKafkaProducer.java b/src/main/java/com/yape/transaction/infrastructure/adapter/out/kafka/TransactionKafkaProducer.java new file mode 100644 index 0000000000..4a267e26bb --- /dev/null +++ b/src/main/java/com/yape/transaction/infrastructure/adapter/out/kafka/TransactionKafkaProducer.java @@ -0,0 +1,23 @@ +package com.yape.transaction.infrastructure.adapter.out.kafka; + +import com.yape.transaction.application.port.out.TransactionEventPublisherPort; +import com.yape.transaction.domain.model.Transaction; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TransactionKafkaProducer implements TransactionEventPublisherPort { + + private final KafkaTemplate kafkaTemplate; + private static final String TOPIC_CREATED = "transaction-created"; + + @Override + public void publishTransactionCreated(Transaction transaction) { + log.info("Publishing transaction created event: {}", transaction.getTransactionExternalId()); + kafkaTemplate.send(TOPIC_CREATED, transaction.getTransactionExternalId().toString(), transaction); + } +} diff --git a/src/main/java/com/yape/transaction/infrastructure/adapter/out/kafka/package-info.java b/src/main/java/com/yape/transaction/infrastructure/adapter/out/kafka/package-info.java new file mode 100644 index 0000000000..f680b033ad --- /dev/null +++ b/src/main/java/com/yape/transaction/infrastructure/adapter/out/kafka/package-info.java @@ -0,0 +1 @@ +package com.yape.transaction.infrastructure.adapter.out.kafka; \ No newline at end of file diff --git a/src/main/java/com/yape/transaction/infrastructure/config/package-info.java b/src/main/java/com/yape/transaction/infrastructure/config/package-info.java new file mode 100644 index 0000000000..3b87604437 --- /dev/null +++ b/src/main/java/com/yape/transaction/infrastructure/config/package-info.java @@ -0,0 +1 @@ +package com.yape.transaction.infrastructure.config; \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000000..fc973711f3 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,19 @@ +spring.application.name=yapePrueba + +# Database Configuration +spring.datasource.url=jdbc:postgresql://localhost:5432/yape_db +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +# Kafka Configuration +spring.kafka.bootstrap-servers=localhost:9092 +spring.kafka.consumer.group-id=transaction-group +spring.kafka.consumer.auto-offset-reset=earliest +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer +spring.kafka.consumer.properties.spring.json.trusted.packages=* +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer diff --git a/src/test/java/com/yape/YapePruebaApplicationTests.java b/src/test/java/com/yape/YapePruebaApplicationTests.java new file mode 100644 index 0000000000..01d29a4515 --- /dev/null +++ b/src/test/java/com/yape/YapePruebaApplicationTests.java @@ -0,0 +1,13 @@ +package com.yape; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class YapePruebaApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/yape/transaction/application/service/TransactionServiceTest.java b/src/test/java/com/yape/transaction/application/service/TransactionServiceTest.java new file mode 100644 index 0000000000..7fb226ba9b --- /dev/null +++ b/src/test/java/com/yape/transaction/application/service/TransactionServiceTest.java @@ -0,0 +1,100 @@ +package com.yape.transaction.application.service; + +import com.yape.transaction.application.port.out.TransactionEventPublisherPort; +import com.yape.transaction.application.port.out.TransactionRepositoryPort; +import com.yape.transaction.domain.exception.TransactionNotFoundException; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.domain.model.TransactionStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.math.BigDecimal; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class TransactionServiceTest { + + @Mock + private TransactionRepositoryPort transactionRepository; + + @Mock + private TransactionEventPublisherPort transactionEventPublisher; + + @InjectMocks + private TransactionService transactionService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void createTransaction_ShouldSaveAndPublishEvent() { + // Arrange + Transaction transaction = Transaction.builder() + .accountExternalIdDebit(UUID.randomUUID()) + .accountExternalIdCredit(UUID.randomUUID()) + .transferTypeId(1) + .value(new BigDecimal("500")) + .build(); + + Transaction savedTransaction = Transaction.builder() + .transactionExternalId(UUID.randomUUID()) + .accountExternalIdDebit(transaction.getAccountExternalIdDebit()) + .accountExternalIdCredit(transaction.getAccountExternalIdCredit()) + .transferTypeId(transaction.getTransferTypeId()) + .value(transaction.getValue()) + .status(TransactionStatus.PENDING) + .build(); + + when(transactionRepository.save(any(Transaction.class))).thenReturn(savedTransaction); + + // Act + Transaction result = transactionService.createTransaction(transaction); + + // Assert + assertNotNull(result); + assertEquals(TransactionStatus.PENDING, result.getStatus()); + verify(transactionRepository, times(1)).save(any(Transaction.class)); + verify(transactionEventPublisher, times(1)).publishTransactionCreated(savedTransaction); + } + + @Test + void getTransaction_ShouldReturnTransaction_WhenExists() { + // Arrange + UUID transactionId = UUID.randomUUID(); + Transaction transaction = Transaction.builder() + .transactionExternalId(transactionId) + .value(new BigDecimal("100")) + .status(TransactionStatus.APPROVED) + .build(); + + when(transactionRepository.findById(transactionId)).thenReturn(Optional.of(transaction)); + + // Act + Transaction result = transactionService.getTransaction(transactionId); + + // Assert + assertNotNull(result); + assertEquals(transactionId, result.getTransactionExternalId()); + verify(transactionRepository, times(1)).findById(transactionId); + } + + @Test + void getTransaction_ShouldThrowException_WhenNotExists() { + // Arrange + UUID transactionId = UUID.randomUUID(); + when(transactionRepository.findById(transactionId)).thenReturn(Optional.empty()); + + // Act & Assert + assertThrows(TransactionNotFoundException.class, () -> transactionService.getTransaction(transactionId)); + verify(transactionRepository, times(1)).findById(transactionId); + } +} diff --git a/src/test/java/com/yape/transaction/infrastructure/adapter/in/kafka/TransactionKafkaConsumerTest.java b/src/test/java/com/yape/transaction/infrastructure/adapter/in/kafka/TransactionKafkaConsumerTest.java new file mode 100644 index 0000000000..735d951c3b --- /dev/null +++ b/src/test/java/com/yape/transaction/infrastructure/adapter/in/kafka/TransactionKafkaConsumerTest.java @@ -0,0 +1,65 @@ +package com.yape.transaction.infrastructure.adapter.in.kafka; + +import com.yape.transaction.application.port.out.TransactionRepositoryPort; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.domain.model.TransactionStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.math.BigDecimal; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class TransactionKafkaConsumerTest { + + @Mock + private TransactionRepositoryPort transactionRepository; + + @InjectMocks + private TransactionKafkaConsumer transactionKafkaConsumer; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void consumeTransactionCreated_ShouldApprove_WhenValueIsLessThanOrEqual1000() { + // Arrange + Transaction transaction = Transaction.builder() + .transactionExternalId(UUID.randomUUID()) + .value(new BigDecimal("500")) + .status(TransactionStatus.PENDING) + .build(); + + // Act + transactionKafkaConsumer.consumeTransactionCreated(transaction); + + // Assert + assertEquals(TransactionStatus.APPROVED, transaction.getStatus()); + verify(transactionRepository, times(1)).save(transaction); + } + + @Test + void consumeTransactionCreated_ShouldReject_WhenValueIsGreaterThan1000() { + // Arrange + Transaction transaction = Transaction.builder() + .transactionExternalId(UUID.randomUUID()) + .value(new BigDecimal("1500")) + .status(TransactionStatus.PENDING) + .build(); + + // Act + transactionKafkaConsumer.consumeTransactionCreated(transaction); + + // Assert + assertEquals(TransactionStatus.REJECTED, transaction.getStatus()); + verify(transactionRepository, times(1)).save(transaction); + } +} diff --git a/src/test/java/com/yape/transaction/infrastructure/adapter/in/rest/TransactionControllerTest.java b/src/test/java/com/yape/transaction/infrastructure/adapter/in/rest/TransactionControllerTest.java new file mode 100644 index 0000000000..4009479bdf --- /dev/null +++ b/src/test/java/com/yape/transaction/infrastructure/adapter/in/rest/TransactionControllerTest.java @@ -0,0 +1,97 @@ +package com.yape.transaction.infrastructure.adapter.in.rest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yape.transaction.application.port.in.CreateTransactionUseCase; +import com.yape.transaction.application.port.in.GetTransactionUseCase; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.domain.model.TransactionStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.math.BigDecimal; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +class TransactionControllerTest { + + private MockMvc mockMvc; + + @Mock + private CreateTransactionUseCase createTransactionUseCase; + + @Mock + private GetTransactionUseCase getTransactionUseCase; + + @InjectMocks + private TransactionController transactionController; + + private ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + mockMvc = MockMvcBuilders.standaloneSetup(transactionController).build(); + } + + @Test + void createTransaction_ShouldReturnCreatedTransaction() throws Exception { + // Arrange + Transaction transaction = Transaction.builder() + .accountExternalIdDebit(UUID.randomUUID()) + .accountExternalIdCredit(UUID.randomUUID()) + .transferTypeId(1) + .value(new BigDecimal("500")) + .build(); + + Transaction createdTransaction = Transaction.builder() + .transactionExternalId(UUID.randomUUID()) + .accountExternalIdDebit(transaction.getAccountExternalIdDebit()) + .accountExternalIdCredit(transaction.getAccountExternalIdCredit()) + .transferTypeId(transaction.getTransferTypeId()) + .value(transaction.getValue()) + .status(TransactionStatus.PENDING) + .build(); + + when(createTransactionUseCase.createTransaction(any(Transaction.class))).thenReturn(createdTransaction); + + // Act & Assert + mockMvc.perform(post("/api/transactions") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(transaction))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.transactionExternalId").exists()) + .andExpect(jsonPath("$.transactionStatus.name").value("PENDING")); + } + + @Test + void getTransaction_ShouldReturnTransaction_WhenExists() throws Exception { + // Arrange + UUID transactionId = UUID.randomUUID(); + Transaction transaction = Transaction.builder() + .transactionExternalId(transactionId) + .transferTypeId(1) + .value(new BigDecimal("100")) + .status(TransactionStatus.APPROVED) + .build(); + + when(getTransactionUseCase.getTransaction(transactionId)).thenReturn(transaction); + + // Act & Assert + mockMvc.perform(get("/api/transactions/{transactionExternalId}", transactionId)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.transactionExternalId").value(transactionId.toString())) + .andExpect(jsonPath("$.transactionStatus.name").value("APPROVED")); + } +} diff --git a/src/test/java/com/yape/transaction/infrastructure/adapter/out/database/TransactionPersistenceAdapterTest.java b/src/test/java/com/yape/transaction/infrastructure/adapter/out/database/TransactionPersistenceAdapterTest.java new file mode 100644 index 0000000000..535d575c6c --- /dev/null +++ b/src/test/java/com/yape/transaction/infrastructure/adapter/out/database/TransactionPersistenceAdapterTest.java @@ -0,0 +1,102 @@ +package com.yape.transaction.infrastructure.adapter.out.database; + +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.domain.model.TransactionStatus; +import com.yape.transaction.infrastructure.adapter.out.database.entity.TransactionEntity; +import com.yape.transaction.infrastructure.adapter.out.database.repository.TransactionRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class TransactionPersistenceAdapterTest { + + @Mock + private TransactionRepository transactionRepository; + + @InjectMocks + private TransactionPersistenceAdapter transactionPersistenceAdapter; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void save_ShouldReturnSavedTransaction() { + // Arrange + Transaction transaction = Transaction.builder() + .accountExternalIdDebit(UUID.randomUUID()) + .accountExternalIdCredit(UUID.randomUUID()) + .transferTypeId(1) + .value(new BigDecimal("500")) + .status(TransactionStatus.PENDING) + .build(); + + TransactionEntity savedEntity = TransactionEntity.builder() + .transactionExternalId(UUID.randomUUID()) + .accountExternalIdDebit(transaction.getAccountExternalIdDebit()) + .accountExternalIdCredit(transaction.getAccountExternalIdCredit()) + .transferTypeId(transaction.getTransferTypeId()) + .value(transaction.getValue()) + .status(transaction.getStatus()) + .createdAt(LocalDateTime.now()) + .build(); + + when(transactionRepository.save(any(TransactionEntity.class))).thenReturn(savedEntity); + + // Act + Transaction result = transactionPersistenceAdapter.save(transaction); + + // Assert + assertNotNull(result); + assertEquals(savedEntity.getTransactionExternalId(), result.getTransactionExternalId()); + assertEquals(savedEntity.getStatus(), result.getStatus()); + verify(transactionRepository, times(1)).save(any(TransactionEntity.class)); + } + + @Test + void findById_ShouldReturnTransaction_WhenExists() { + // Arrange + UUID transactionId = UUID.randomUUID(); + TransactionEntity entity = TransactionEntity.builder() + .transactionExternalId(transactionId) + .value(new BigDecimal("100")) + .status(TransactionStatus.APPROVED) + .build(); + + when(transactionRepository.findById(transactionId)).thenReturn(Optional.of(entity)); + + // Act + Optional result = transactionPersistenceAdapter.findById(transactionId); + + // Assert + assertTrue(result.isPresent()); + assertEquals(transactionId, result.get().getTransactionExternalId()); + verify(transactionRepository, times(1)).findById(transactionId); + } + + @Test + void findById_ShouldReturnEmpty_WhenNotExists() { + // Arrange + UUID transactionId = UUID.randomUUID(); + when(transactionRepository.findById(transactionId)).thenReturn(Optional.empty()); + + // Act + Optional result = transactionPersistenceAdapter.findById(transactionId); + + // Assert + assertFalse(result.isPresent()); + verify(transactionRepository, times(1)).findById(transactionId); + } +} diff --git a/target/classes/application.properties b/target/classes/application.properties new file mode 100644 index 0000000000..fc973711f3 --- /dev/null +++ b/target/classes/application.properties @@ -0,0 +1,19 @@ +spring.application.name=yapePrueba + +# Database Configuration +spring.datasource.url=jdbc:postgresql://localhost:5432/yape_db +spring.datasource.username=postgres +spring.datasource.password=postgres +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.hibernate.ddl-auto=update +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect + +# Kafka Configuration +spring.kafka.bootstrap-servers=localhost:9092 +spring.kafka.consumer.group-id=transaction-group +spring.kafka.consumer.auto-offset-reset=earliest +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.springframework.kafka.support.serializer.JsonDeserializer +spring.kafka.consumer.properties.spring.json.trusted.packages=* +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer diff --git a/target/classes/com/yape/YapePruebaApplication.class b/target/classes/com/yape/YapePruebaApplication.class new file mode 100644 index 0000000000000000000000000000000000000000..e28dc8e681d4408d48e29cede579447201127b1d GIT binary patch literal 735 zcmaJUs4L?dxB6g5>U#A$67v((;=)@$1SEKXD$ z_yPPV)LDas4^}OCM)Ue+_M5qXxV;1L8Sgr1U?D)Wixw6cdY9r#@Kor8e~m6>>=+h5 zDXrWd!@^)V3(#hGADfK-5p&5;NF3WjMq+_H zrLxk3+&(;JXim(TWLOE6mPbVvN&DUBGW0_ei*zQe^7fOn=`K{xuoM3O?H)H1ir%m> zs2h1kabXieTCE8a|A*3Dn6qQynR3!nVrOa!8_NUbXWOo2{G(@!Ve4C=oyz1)<;uTW zYvam|5XxRHS>+l;;S8}>vL&CE@`d(OVQq{L2}Oiy~6#MLYuz@bF5A;Q9@Qm^tV zufDq8&KCd!Gy)U|Lv0du;!KpYoaU@uWKzXh7Dt+&eP*mvQL08(*vazcTIDk0TmqB` z%{?Dk#hh-{Y_k{b2rd1&Kv;JsU-Lqfu#ZSgD6x8r=y(70`%9+cb|##&V6}{+SP1Q3RJ{jKg+M6!Zvo^X%cyuAcvU7;A4Al9Euek}kyvUU literal 0 HcmV?d00001 diff --git a/target/classes/com/yape/transaction/application/port/in/GetTransactionUseCase.class b/target/classes/com/yape/transaction/application/port/in/GetTransactionUseCase.class new file mode 100644 index 0000000000000000000000000000000000000000..35ea60932f8ea2e0a4023a7f064e1c6e133ed99e GIT binary patch literal 311 zcmZvYO-chn5QSeQCSxK4;v>u#Q1hoW$|;I(#UvX%OVc=OWV<&ZCbpg*H7$0LvejE@ zwZIW!w32sWCw|FKuPZf)aJu}LCftl?+tfN)$7MOan_SEeqN`9#rq!WR$9up@=FBy{ zh3Z+N)S5K5gtNWY_aE8w;-(d0_!8F9=!11iI6qkZa?__zAawIT0Lqv2(9gQasz*59 Km>A^Q!SD~)QeC_N literal 0 HcmV?d00001 diff --git a/target/classes/com/yape/transaction/application/port/out/TransactionEventPublisherPort.class b/target/classes/com/yape/transaction/application/port/out/TransactionEventPublisherPort.class new file mode 100644 index 0000000000000000000000000000000000000000..025f3d21cabe65075c79a838ce218bbd37e7a316 GIT binary patch literal 314 zcmZvYF;4?A427Rd+k6fFFfOVc;ZK zEI;d6-?KkH-`)Y-U|wNFxVN!2ubjka=FqWK7lYxHytDk>rgS!SZbuR%r-Vi-&*dO>Ef()5b>pka~TwE{sRf zZER}J+=@!>2=hU7!t6Qr*~*>sLO36A|AxLi z2kWaDcVn~!1}j>VG?Xvp^kt=zAkZpktpuKjlV3?vn~~0CW>aOhmygbN#54K53U4-@ zk*7=r4o0Fff^$&P+hGcHejV zsXwE&RjWOxr=NTJn|k`r0vSj^@SJ`~X6DYl&$~bGJAeQC=uZIW@nbUrXpqn-V+VFJ z#B!n(T~^9GY8i@dDmhCPdQ>TwH8rO=_a-+M)f|t`RNtrFC58q~6dA%PVHBgLRyaK$ zO^ITW8?h!dF?7WZP8fPI;=_&XR2GMb)Dv zk>{FU5r&Q;w}xG`#0`rZx}qiXqdcoxq*r%OYF=4Xq7_TkqO-Hf(O3&Y*d-w><7w<> z2(4F~p;6Tfg5g-VdsI{OVl-_TWJS!kGL@a@IV;waX~tf(OL#^`2lg?X+LrohOR*{@ zgEXC(98FG+Gn`310YJ?5;Q<-X;vhq_$*mRKv)xYwXIm9OM8+W;X4vBk-X&fZrfLae znITah+n+jRVOUX7vHT&cLQKiWv{{(Rk=OtW_@e*DpCaBWDiV%htVI(w@&&(NOiQ$A_ zLM@=Dr$Yx`@dq)2xP(y|35*e!I5K=27WiwWW;H&n>w?&s@{ybw zPHCc)6${aEu5&}Ncs|CkD`$5%?n5KuiBpGSXiKR&pRAO!+?Y`a%xRH1Maw9LYCn66 zjn=;t5L4jcDiQl!PIc4n%S`e+++7$vUFmiaYygeJH(C-T#Zp_DjvRnoG<^H8Ra ze}>%XmhCQCu#YSWV`1$;vFc8LZd0i3>P}Ni*}T%V)>U1{88X{+*48+N!qRju zr+MrZOpA(<<729w$?d*ad2`^P&3p{qb+f>v^x?uzoqZ4STmTbv+tdFL0|E5^O#1_Pi~jc#8gKxu=)@k962TGl z(%}6ztwvm2T!%tWBmr6ENOlA=@^lvfr_~*dmVpBOY=w(-WtfBNsh6g00a|PKG&vPO z-y^&L+(+y$1gn~~I?@K{Yiy9T8gMk&>mFUzpdIsAAb3yiyeGHCk;|Z6MTu?`&^_T3 zbl&z&Tkiu5_x9aGXK(*K^!!3guKBjcpbx*QxoxY$+8npr@7wHoo=(_en*+2QqJ@w@ zMxmfxcLZifV1ozEA$J~>LRIe8}H*Id_w2G!Z)-Yq`PnNJ?^06Kd;}< AMF0Q* literal 0 HcmV?d00001 diff --git a/target/classes/com/yape/transaction/domain/exception/TransactionNotFoundException.class b/target/classes/com/yape/transaction/domain/exception/TransactionNotFoundException.class new file mode 100644 index 0000000000000000000000000000000000000000..a7d4c56d74d6b8924b85f290f5a6ac4876b9de8e GIT binary patch literal 489 zcmbtR%T59@6g|a(QA9zEF5I#6k=PGFS@>Wz5+j8DWu}1|rX`(@(a&;a;=&K`ql~vc zf{BS6+vN71+tYJ;+t;_}7XYW&uA_pL7*PXN)CBf!>5k-_%v9c##_L>N-luBewKW2@ z6K%Bb3GD1AOWx2sZKmCWag0@gv()DDffh=7N2Z`O9F>{PNgJuYwd82IIk3KOOOyRD ziq37O1e%F9YEb4A)P^`M!RoYuRn#G0gRiy!W#dp2O{Wn`Y7orqY#`Z9aKY zuay<`IRk0MJI`A!x9iB>fSPDpQn{QrkX0@V5tphb+S=!WfjP`GoC~;r65h`7p-`l= znlxB{-H~EXcmeOb26A|hVe*LET|xZXufyfOS+VwNqGffs9k{-4UoeSH z3>Bp9U|0Zk*}yVZ7^YjE;8L{LX*Y73Sy~-oHry7sYxHG@-pUE&8Lkh-mBK1MP&@x2 z!*7MN0UzxBRIK8|@}C0m#&s5Vvn1W3ffBCLHh1`esJc#z%cqvyryj-Kkrek=|C
r|BM;_k|-or41S~QKbXN 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 Rx+u#Uox)G>GyDS9{{XYS9ohf@ literal 0 HcmV?d00001 diff --git a/target/classes/com/yape/transaction/domain/model/TransactionStatus.class b/target/classes/com/yape/transaction/domain/model/TransactionStatus.class new file mode 100644 index 0000000000000000000000000000000000000000..bd76fbeb5ebedefde9a56944901d2bb37892fa25 GIT binary patch literal 1324 zcmbVLZBNrs6n^g7ty}G2Rs@;g3r^WUsK|?$A%VaompsZa2dS_+?FQ&pqcp=Q%I;+~0q`{{rv|FBQZXW-Y&NeBvK%BMi9L;8y7P zo>BAb-0_UMU$b3f_u{||dDv{Ez#wagBf+3lN?YrjTN@1Xpqu6CI z!@%(liro7@)c!F`!k|>P^ z!(zVBTT>|fKMr|~uAZU4?0EK8v%YT!yL{gz%jPwC6?(b?2EFWC+^zD!5!{^svd8Nb zN59%QhRj<#JoIZ79uOw911eSy4;>O}&Zbu8+P>+w;s-Uy<1RzWbPhZk+<9^G?Tk`Um;y z52RB(Jb@G`V{)V@k@6@WDe4(=CrFFT+njpjFC$HT&QiwXRA8)=<^o<2E{4Uwwwf(@ literal 0 HcmV?d00001 diff --git a/target/classes/com/yape/transaction/infrastructure/adapter/in/kafka/TransactionKafkaConsumer.class b/target/classes/com/yape/transaction/infrastructure/adapter/in/kafka/TransactionKafkaConsumer.class new file mode 100644 index 0000000000000000000000000000000000000000..7acd37f49f9a9492c3eecccc24135e1a433bf3b1 GIT binary patch literal 2426 zcmbVOTUQfT6#h;?62c%Dyv1v^B7CFurXQ}_&B zj%+YwD$;M5fwMljX;x&TA^c(rDTZrRVF`O%)N?UwIm;JZ(V5s5uDX}|;m0)6=u5&d z(2wH`V;wQpWs?)2rmPDm4v67wL#PRZ;Zz4e z+I$vQ419nO8G4C1kNaYU;Ah)!@3T?N&-daQt|u{KAdgXoTZf{1S#cEx`-)Ao^pG29^Bflzyj?~WWvUB0H>8aHrL`;?klTByGeXv;2w$$rH-5&PA4Tp zz_%%vULKN@V!pFW2h|W&>wSjdSP|udD9*Xit4Fn}P|PUR_hNR3a=oL! z2N`S$QVx_bn?zY>`W)3_R1{IYwW^Ey%ZSeKN0?9Iv4JP3Fr3+6v2)bLqCjd$t@guk zHC`+whX(z`Zum!3CiGln7A@MY-fS#TnLADuEhIv`w! zt5X^R)N-aQr`@R2%QEyWF=y)-UW}#5|Ayxco$U;x9o;7Pd?D2i4eAp0bm`KlEMl0t zS&8O)0_XHN2hDTZ>E}~=W^|jQXBX`ku|&_$Y43yK`3M`W>?4tKA2D(6tQ-2}* zJahgHF79IZC-fIy;p#35zaUL3lUAd@VWRLBA1Clk3r~^EbR_(1WTi^~myo3Y(>P93 zXqZqHZ~+s@(U-?CRqGWjN4JrEtYDRPgakD_BaC0rny?jm@SIl17##y&(r<_^Ul8c8 zUVsJo8s7x?mX0ptW!tA|y3{(;qi^tFwD3D7G@=AFBFzRc5VM?qXE_})#P;k>(x*V2 oz!W&f`T_iz1B&M{6B8lA5tGA z(ZnC%k22m}5YQ9@31O3+$(?)W-ZN*;ou9wH{Q&R`j|<44Cy#6qIrK6NZi+3zL!q0z z*4UK3W9Xe#TDdufo>F-uk4p?w#x{A>u}U}JThW%=#(v-}=Q?~%MjfLgxul(Py9E?* zIgerySJ2P!+&68$D>{-pD|94$r;O%-X$z&f-0@{6U06SIR*hRWu@090I2j5}=~iVF ziKb*2E|n`sfa@+n_?SMwMu;u?k-CQbyqLWt{5xHw|ytJc=vE!SRG7a1lir`X~Z zkjF^E+6{)e(lHqOym_ZdvS0$~C8UlryzI)3(!_A&Z0x((N-hG?anf=k%*x2|7olm0 z@Q|Jrp@UFbhHR3VVW6V4tj6tzwCkb~(#lZ9_#)g8RweeqV%D`3aZst89|VtKyp^K) z|8<&D_o8P~g;zeA?7tOBurhLoVevewQn##1*D}E?k#qswOCqGYYHHGyC9*$W?BQmQ ziSVEo=IFqqYcX|WTW+XG(Vfq0ZAkRgdW80ASK_v1gEy28xHydE|7wg9=e9%`(=iKm z6I)*{t3+U9-6i=XY16aRTQJ&L6Ox+egDvGt4-asY-m2uwfQqA+##>}@niN| zse6pZ#0`3YZqr!Y55OJVC5z!6?jxISUZ6QiI`AF&H#1++_XPucxc-TjGB`@ppQahv v4?Rdjn+Vf5rJJDW93umGm~NcJBbxikHAR2vEtpS%8I)7+bm~p;%>cgv2Z*gk literal 0 HcmV?d00001 diff --git a/target/classes/com/yape/transaction/infrastructure/adapter/in/rest/TransactionController.class b/target/classes/com/yape/transaction/infrastructure/adapter/in/rest/TransactionController.class new file mode 100644 index 0000000000000000000000000000000000000000..c8b1bfd81dca99e5f7463c47430d66fbf1f71b44 GIT binary patch literal 2872 zcmcImYi}Dx6g}fSj1xC@3?WHNOKB;MozUUYmV%Se)JaoQ2O{So6}~XG2WOl0uGt-@ zM2N3^;m-h~Dinzyz>h*byH0Hw@yNK%o@5k7_n;tE$31o33Yq9SVJ%Su1%CG?nW~grHb_^50u0u zycx&kB;LXmLLOKWk?sU0jsvC0j!0-3%a%Jk#nn?sJK*;1B(C8dftb55uoS^-wm-4Q z<-kzQAsa_7h}OJ7`ecX%hUT)n0$-k9#eJ1>0)oJB5MzN<$#V4Ptwu$A+p1!7BvW#$ z%HCC;74(k=hs~ZvQ9g_g_E}0zUSPbZTnWf%-+fb-NZXlT>yahd+|Xvvt!=6xBU$i> zI(a^h=Z;|X3$8nkdtlmU@D^gESbVXJyrzTa$ zR&Kh!*6|R8PRyzGHA4WfyCVS-VPg z6I8%H`$lI8#La=pX4UC(J#wJz7F9JPzmNR29|Ud>?0Hj}y_ac6s(hP zWR#1@YRWS};KGrhiq1iVOfx0hZB*QSxvm}U@#;_`_OUIu?#69Ws;1R@Tk?7M#8{TWcP6-1v6Tr9K7NP5QiTPMzRTVt z408mS&iw{4{|DkRZ2XCHj~0GK>KUef=2Q$f_#WezW?VW0$=2oeGQ5ZP*$ZYqz=xsh zByO@5!!3@E2o3|c`NZ%M?(jQGqmTLhgilF^K@H?)a z#Bz~XF7;qZcd(>+t6UDTjD)B%%`!pFTXrb38m8mmMS(6n)c zDV2MM>$&--5V?h?nEi!QZF+*g0#ZG2QXM#{FaquQAO>wXi5Q2|3`!bnTpBpIAG%u) R-w#5785$Sa|EvS@;R`NeYAgT% literal 0 HcmV?d00001 diff --git a/target/classes/com/yape/transaction/infrastructure/adapter/out/database/TransactionPersistenceAdapter.class b/target/classes/com/yape/transaction/infrastructure/adapter/out/database/TransactionPersistenceAdapter.class new file mode 100644 index 0000000000000000000000000000000000000000..88f743331eb97797ddc85681962c4c3536c2d185 GIT binary patch literal 5463 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 H4>0~0jV|2t literal 0 HcmV?d00001 diff --git a/target/classes/com/yape/transaction/infrastructure/adapter/out/database/entity/TransactionEntity.class b/target/classes/com/yape/transaction/infrastructure/adapter/out/database/entity/TransactionEntity.class new file mode 100644 index 0000000000000000000000000000000000000000..5f50fe8d03352284f700af51db6ff92d2907485d GIT binary patch literal 6979 zcmd5>S#uQE75-Y9neG|2q(MSLjldu*Vk9IFNpP@7K#atuM8X&eh;bITMvc^Hrdyur zAtw92IE&+~j^jAHNnBKAsR*jTq(>q%#O*H;g*)kx?5g!syS##nMfm}A#>36+~BZ= z_B|u#vM`Xd zP2@QFVH3OYHVsC}_KrPSx654LJ+V1GvT(^M1rs9|1bB~$5sYfswB!UbGGYtSw`U|~ z7A-{%V;I+QzlnnAB`-ZB7fTUy){f}4Cg|U8;vIOWhJlD)MR3Dq8hWB$8eSHb514qD zu(W7-B+=0V^`MD|1Zry%O8HvACQKZ}Aub7F=alybqHdf0C-Cn!@i2~X(TP{S&~ShT zcjH9CH)Y}|j%hH-$5BOo9E`@0=*cmhFmV#o9QLwZTb^Q&$PGE&!vyhO6Em1)gjehp zXUg|Vc5vPe2=W+mUciuhHS~F7S(P(4`o1mhl}Ymw-PJlMs)U)wgKRFSy517Qcnm4} z{tUM_!}^?Ca~I0a5zq6vF_-~4y#eyA*ww%;tUA@2TMHboQZ6ZT@X1kVD4|MNkK-I7%i8KQBYYWbGE)JkAyN~M_*8x zUW2z@EdNhV7O`?Fdb?*_&pB1EaP;$Hg)|I?E}r%-CpfuhrtDW1{FTBj=aH*Nro3}@ z)fE&WPbye;85MKMq4}Roi#-n1!sOVRy?R@A*~mF2#aSm<_7_jvRlDNwIOE7HawO+5 zX0Ifj%3jROQRlJD+#k>9QDPPy8w7_5&I&Qjn^(ZHghhGn!9B@qw!zsXXiUSW`{Tie&Mj`TTc9##+?3LJe&6*sP|7)+&$KD(r{)Hpl3l|I(eNspFRX6@B*OQcVq znV#jLqwxgERGnJA%+jH|Wg+`jOY1Ui8D4J<23j@I{4&GgZ1mx1=(*w8AdSFRIbOr& z==75(PMG;Myxg**RLuPUom-6Q+?-#pmYn0R$YuSqfC?8<7TfoWXkYKZQ1v+De=sro zOD|HJ@|Z)o*W|uXZ=p6ogDfu)^eMnJi}tj z5dL_qDXK|hO(DgUT`teLfiq#?CmNn@S>iphb!n89hqSSs$Qe6)iHJ zg`nbDNVnqokTyckOna;>rpk7$=Fk@dEkg`1SUP^8VRwrck``CTRTc$r4dtd*p1=?* z%rTZ4yfKO^8cU8x__c{&r}=)A-)9`mVV(`VIECsCzR&VGcLC?A;B_fk!8=m2f_I^0 z1#do~qMyX?qii{N3>V_v@k$e7x`}z5!xUqAzZgp&viJZ#$mS2Rc_-iEpYbZR$8Mng zHa zZQGED_mc#od^6FG4T*S9OCaiPCfcNd(YQA^lJbJ%3lC=-W;*=r%D`4ecF$3F*|ey|2S~ z9_jXLd;bVyc5L)I?v{Il4C5v)v#p)^@*nsxr4&x!uXu`58ngHdu29NwGpykwlyrLj z*Z3$U1JB|c_!y-uzK5sraY{K{#|l0{sRMr?|0gM#_#2MnQeezC#Ahkx@j1@nJO*^*pXkzI+(f^QG5$BSn{1V0_HIRl!B%Suwv_nwUl_{|ab-k~ zON%HI3%Xtsj1EC@9M(B>En=@Xv1{U2Ql#PG-1a-Zb0mE|KXEX1NCxb-{KPWD)yVI? z3Vr-Kjz1yV_Z3q@VWmQiA%a+`_7~Gq#rTy*hLw&~2Z|Y~%G^>hs+Co-s7n*yQ3zZHTcO$Yij-0!N;oS}0>HMmQJE1&&*4)D2;Pm#}IK9jHD zJwS6+qVkfeC3hv(lKh)W*h`}*y@6BBM@#R97VQ|#a*U)m5ZI=C=Q9MyXW$39Qh}`nJAkxQ}ydShnNmKzT zDTxC+EhTZbW~3yJL0#sK?Yb7f_&I*0_@UwZlnlO~=ksIylwUmr^VeSZ8TZl8@e+Os>)&-+6o&u+ literal 0 HcmV?d00001 diff --git a/target/classes/com/yape/transaction/infrastructure/adapter/out/database/repository/TransactionRepository.class b/target/classes/com/yape/transaction/infrastructure/adapter/out/database/repository/TransactionRepository.class new file mode 100644 index 0000000000000000000000000000000000000000..58217e74fc1c4e0ce6147e207a0e7cf631f8eedc GIT binary patch literal 535 zcmbV}!Ait15QZnKZrxSEtG7zlVQOoJ_1iR?&wDp(nfN_Wl8Nu)VhmJUr| zoD)+{UZRuMc*VxGc)xr)`ROLGDPX^%7s8r!iFbJ=3r=7t;KaBRy>&{L@t;P%8u#3T zv9k0Vd~0b56fl~qQd1{Mz+3hoon~ucPqVcxkeU_$8UL#9q~F>Cs~Tsso6Gb(6)>Kf z;0k%I>Zty3rDez6NCFNfAz~W&puEa!d7-so>T>Y{j#duoV>;4s8)IDhl}4upY{az{ X1BZA@;y!?F81;1wJFyPq{Vu>4#G$nn literal 0 HcmV?d00001 diff --git a/target/classes/com/yape/transaction/infrastructure/adapter/out/kafka/TransactionKafkaProducer.class b/target/classes/com/yape/transaction/infrastructure/adapter/out/kafka/TransactionKafkaProducer.class new file mode 100644 index 0000000000000000000000000000000000000000..9c2416cdbe4f4947f597dea487fe1fad0a6310a0 GIT binary patch literal 2089 zcmbVN+j1L45Iti#k-V0jp!gCe1Y{sYk}pd_fDjpjkuQiSjw)7^CkjTZvAp(bMm0NY za8Y~|w{Usj1M*Q6Ju4ZngIs0vvNJu^)7|Iv>DiZm|M?eyPq00P5v0->&EgU+Gi-aR z>mKo*aE<0N ziwR6JEDbHQt-9QoZdbKMkSvMedPkTWhZmcyn)@>apgbyx~Q$@+paA}%6h^UsiUVo?)h%5bhX1G_V4W;a? z(WT)baaCxd8LrfPDYm0-OX!A`MEtCHJZN(5TYGRgYW94htUf%^|9;Adjq$|TF}TAM zoQZ_g`dFAf)vj|p=JWzlm3IplYFtdmOid+GAN7urKfhS8bE?30AhY5HgRydURHQwz z>Dva}O7!^D!@q2GLQ)v0x;aGCrJ=|};zcM!pyKIQl18PBd3rz#V0a%X+^~Nt+J8Vl zFuQ(8YtFu{wEu|qBX~eQALAj}<=CF1TMR4oyBonOoqUB++5z)Ff#uQ*FEI88uB1@< z1=G1XYreouTIQes7+?RCzLOLJn4)zWdCcH5vS#{0u!hgcqHw5Sof2)({t_btWGrBl zve~S4a<^HX(r>s^TKE;yHhc;;yu|@z6Jecy!#V?OTd{}rvnLNu63x=@st^2&Q>0{2 n#Ui=c