diff --git a/README.md b/README.md index 7f832ad..6d19c2e 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,13 @@ # Yape Code Challenge :rocket: -Our code challenge will let you marvel us with your Jedi coding skills :smile:. +For the setup you only need to run 2 commands, that is if you already have docker installed. I assume you do :D -Don't forget that the proper way to submit your work is to fork the repo and create a PR :wink: ... have fun !! +Be sure you are positioned on the root folder that has the docker compose file and run these commands: -- [Yape Code Challenge :rocket:](#yape-code-challenge-rocket) -- [Problem](#problem) -- [Tech Stack](#tech-stack) - - [Optional](#optional) -- [Send us your challenge](#send-us-your-challenge) +docker-compose.exe -f .\yape-challenge-environment.yml build -# Problem +docker-compose.exe -f .\yape-challenge-environment.yml up -d -Every time a financial transaction is created it must be validated by our anti-fraud microservice and then the same service sends a message back to update the transaction status. -For now, we have only three transaction statuses: +To help you test my submission I prepared a Postman Collection you can import which can be found in the root folder too. Root folder also contains a Report I made that I invite you to read. -
    -
  1. pending
  2. -
  3. approved
  4. -
  5. rejected
  6. -
- -Every transaction with a value greater than 1000 should be rejected. - -```mermaid - flowchart LR - Transaction -- Save Transaction with pending Status --> transactionDatabase[(Database)] - Transaction --Send transaction Created event--> Anti-Fraud - Anti-Fraud -- Send transaction Status Approved event--> Transaction - Anti-Fraud -- Send transaction Status Rejected event--> Transaction - Transaction -- Update transaction Status event--> transactionDatabase[(Database)] -``` - -# Tech Stack - -
    -
  1. Java. You can use any framework you want
  2. -
  3. Any database
  4. -
  5. Kafka
  6. -
- -We do provide a `Dockerfile` to help you get started with a dev environment. - -You must have two resources: - -1. Resource to create a transaction that must containt: - -```json -{ - "accountExternalIdDebit": "Guid", - "accountExternalIdCredit": "Guid", - "tranferTypeId": 1, - "value": 120 -} -``` - -2. Resource to retrieve a transaction - -```json -{ - "transactionExternalId": "Guid", - "transactionType": { - "name": "" - }, - "transactionStatus": { - "name": "" - }, - "value": 120, - "createdAt": "Date" -} -``` - -## Optional - -You can use any approach to store transaction data but you should consider that we may deal with high volume scenarios where we have a huge amount of writes and reads for the same data at the same time. How would you tackle this requirement? - -You can use Graphql; - -# Send us your challenge - -When you finish your challenge, after forking a repository, you **must** open a pull request to our repository. There are no limitations to the implementation, you can follow the programming paradigm, modularization, and style that you feel is the most appropriate solution. - -If you have any questions, please let us know. \ No newline at end of file +I am thrilled to hear back from you and receive the feedback about my hardwork. \ No newline at end of file diff --git a/Report - Analysis and Roadmap for the Yape Code Challenge.pdf b/Report - Analysis and Roadmap for the Yape Code Challenge.pdf new file mode 100644 index 0000000..8c736fb Binary files /dev/null and b/Report - Analysis and Roadmap for the Yape Code Challenge.pdf differ diff --git a/YapeTestsPostmanCollection.json b/YapeTestsPostmanCollection.json new file mode 100644 index 0000000..564111e --- /dev/null +++ b/YapeTestsPostmanCollection.json @@ -0,0 +1,124 @@ +{ + "info": { + "_postman_id": "2ca7e9fd-341c-43ab-b788-b1dd53be86b5", + "name": "Transaction Service", + "description": "Collection for creating and retrieving transactions", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "7764285" + }, + "item": [ + { + "name": "Create Transaction JSON", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"query\": \"mutation { createTransaction(accountExternalIdDebit: \\\"1234\\\", accountExternalIdCredit: \\\"5678\\\", transferTypeId: 1, value: 120) { transactionExternalId accountExternalIdDebit accountExternalIdCredit transferTypeId value transactionStatus createdAt } }\"\r\n}" + }, + "url": { + "raw": "http://localhost:8080/graphql", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "graphql" + ] + } + }, + "response": [] + }, + { + "name": "Retrieve Transaction JSON", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\r\n \"query\": \"mutation { createTransaction(accountExternalIdDebit: \\\"1234\\\", accountExternalIdCredit: \\\"5678\\\", transferTypeId: 1, value: 120) { transactionExternalId accountExternalIdDebit accountExternalIdCredit transferTypeId value transactionStatus createdAt } }\"\r\n}" + }, + "url": { + "raw": "http://localhost:8080/graphql", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "graphql" + ] + } + }, + "response": [] + }, + { + "name": "Create Transaction GraphQL", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "graphql", + "graphql": { + "query": "mutation {\r\n createTransaction(accountExternalIdDebit: \"123e4567-e89b-12d3-a456-426655440000\"\r\n, accountExternalIdCredit: \"123e4567-e89b-12d3-a456-426655440000\", transferTypeId: 1, value: 120) {\r\n transactionExternalId\r\n accountExternalIdDebit\r\n accountExternalIdCredit\r\n transferTypeId\r\n value\r\n transactionStatus\r\n createdAt\r\n }\r\n}", + "variables": "" + } + }, + "url": { + "raw": "http://localhost:8080/graphql", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "graphql" + ] + } + }, + "response": [] + }, + { + "name": "GetTransaction GraphQL", + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "graphql", + "graphql": { + "query": "{\r\n getTransaction(transactionExternalId: \"22c6d270-77ea-4afd-89e4-16a593df4af2\") {\r\n transactionExternalId\r\n accountExternalIdDebit\r\n accountExternalIdCredit\r\n transferTypeId\r\n value\r\n transactionStatus\r\n createdAt\r\n }\r\n}", + "variables": "" + } + }, + "url": { + "raw": "http://localhost:8080/graphql", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8080", + "path": [ + "graphql" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/antifraud/.gitignore b/antifraud/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/antifraud/.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/antifraud/.mvn/wrapper/maven-wrapper.properties b/antifraud/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..aeccdfd --- /dev/null +++ b/antifraud/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.1 +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip diff --git a/antifraud/Dockerfile b/antifraud/Dockerfile new file mode 100644 index 0000000..e487b5e --- /dev/null +++ b/antifraud/Dockerfile @@ -0,0 +1,26 @@ +# Base image with JDK and Maven installed +FROM maven:3.8.4-openjdk-17 AS build + +# Sets the working directory in the container +WORKDIR /app + +# Copy the project files into the container +COPY . . + +# Build the application +RUN mvn clean package -DskipTests + +# Verify the build output +RUN ls -l /app/target + +# Small base image for runtime +FROM openjdk:17-jdk-slim + +# Sets the working directory in the container +WORKDIR /app + +# Copy the JAR file from the build stage into the container +COPY --from=build /app/target/antifraud-0.0.1-SNAPSHOT.jar app.jar + +# Specifies the command to run on container start +CMD ["java", "-jar", "app.jar"] diff --git a/antifraud/mvnw b/antifraud/mvnw new file mode 100644 index 0000000..ba9212a --- /dev/null +++ b/antifraud/mvnw @@ -0,0 +1,250 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.1 +# +# 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 +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl="${value-}" ;; + distributionSha256Sum) distributionSha256Sum="${value-}" ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/antifraud/mvnw.cmd b/antifraud/mvnw.cmd new file mode 100644 index 0000000..406932d --- /dev/null +++ b/antifraud/mvnw.cmd @@ -0,0 +1,146 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.1 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/antifraud/pom.xml b/antifraud/pom.xml new file mode 100644 index 0000000..1583c4a --- /dev/null +++ b/antifraud/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.0 + + + com.yape + antifraud + 0.0.1-SNAPSHOT + antifraud + AntiFraud service for the Yape Code Challenge + + 17 + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.kafka + spring-kafka + + + + org.springframework.boot + spring-boot-docker-compose + runtime + true + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/antifraud/src/main/java/com/yape/antifraud/AntifraudApplication.java b/antifraud/src/main/java/com/yape/antifraud/AntifraudApplication.java new file mode 100644 index 0000000..05b16c8 --- /dev/null +++ b/antifraud/src/main/java/com/yape/antifraud/AntifraudApplication.java @@ -0,0 +1,13 @@ +package com.yape.antifraud; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AntifraudApplication { + + public static void main(String[] args) { + SpringApplication.run(AntifraudApplication.class, args); + } + +} diff --git a/antifraud/src/main/java/com/yape/antifraud/config/KafkaConfig.java b/antifraud/src/main/java/com/yape/antifraud/config/KafkaConfig.java new file mode 100644 index 0000000..f9fa848 --- /dev/null +++ b/antifraud/src/main/java/com/yape/antifraud/config/KafkaConfig.java @@ -0,0 +1,72 @@ +package com.yape.antifraud.config; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@EnableKafka +public class KafkaConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Value("${spring.kafka.consumer.group-id}") + private String groupId; + + @Bean + public Map consumerConfigs() { + Map props = new HashMap<>(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "transaction-service-group"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + return props; + } + + @Bean + public ConsumerFactory consumerFactory() { + return new DefaultKafkaConsumerFactory<>(consumerConfigs()); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + return factory; + } + + @Bean + public Map producerConfigs() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return props; + } + + @Bean + public ProducerFactory producerFactory() { + return new DefaultKafkaProducerFactory<>(producerConfigs()); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } +} \ No newline at end of file diff --git a/antifraud/src/main/java/com/yape/antifraud/listeners/AntifraudListenerTransactions.java b/antifraud/src/main/java/com/yape/antifraud/listeners/AntifraudListenerTransactions.java new file mode 100644 index 0000000..ff50fdd --- /dev/null +++ b/antifraud/src/main/java/com/yape/antifraud/listeners/AntifraudListenerTransactions.java @@ -0,0 +1,21 @@ +package com.yape.antifraud.listeners; + + +import com.yape.antifraud.services.AntiFraudService; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Service; + +@Service +public class AntifraudListenerTransactions { + + @Autowired + private AntiFraudService antiFraudService; + + @KafkaListener(topics = "transaction-events", groupId = "${spring.kafka.consumer.group-id}") + public void listen(ConsumerRecord record) { + antiFraudService.checkTransaction(record.key(), record.value()); + } +} \ No newline at end of file diff --git a/antifraud/src/main/java/com/yape/antifraud/services/AntiFraudService.java b/antifraud/src/main/java/com/yape/antifraud/services/AntiFraudService.java new file mode 100644 index 0000000..43ecbd2 --- /dev/null +++ b/antifraud/src/main/java/com/yape/antifraud/services/AntiFraudService.java @@ -0,0 +1,18 @@ +package com.yape.antifraud.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +@Service +public class AntiFraudService { + + @Autowired + private KafkaTemplate kafkaTemplate; + + public void checkTransaction(String transactionId, String value) { + double transactionValue = Double.parseDouble(value); + String status = transactionValue > 1000 || transactionValue <=0 ? "3" : "2"; + kafkaTemplate.send("validated-transactions", transactionId, status); + } +} diff --git a/antifraud/src/main/resources/application.properties b/antifraud/src/main/resources/application.properties new file mode 100644 index 0000000..db447cb --- /dev/null +++ b/antifraud/src/main/resources/application.properties @@ -0,0 +1,13 @@ +spring.application.name=antifraud + +# Kafka Config + +spring.kafka.bootstrap-servers=kafka:9092 +spring.kafka.consumer.auto-offset-reset=earliest +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.group-id=anti-fraud-service-group +spring.kafka.topic.transaction-events=transaction-events +spring.kafka.topic.validated-transactions=validated-transactions \ No newline at end of file diff --git a/antifraud/src/main/webapp/META-INF/MANIFEST.MF b/antifraud/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 0000000..254272e --- /dev/null +++ b/antifraud/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/antifraud/src/test/java/com/yape/antifraud/AntifraudApplicationTests.java b/antifraud/src/test/java/com/yape/antifraud/AntifraudApplicationTests.java new file mode 100644 index 0000000..ac8d891 --- /dev/null +++ b/antifraud/src/test/java/com/yape/antifraud/AntifraudApplicationTests.java @@ -0,0 +1,13 @@ +package com.yape.antifraud; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AntifraudApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/transaction/.gitignore b/transaction/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/transaction/.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/transaction/.mvn/wrapper/maven-wrapper.properties b/transaction/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..aeccdfd --- /dev/null +++ b/transaction/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.1 +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip diff --git a/transaction/Dockerfile b/transaction/Dockerfile new file mode 100644 index 0000000..c560750 --- /dev/null +++ b/transaction/Dockerfile @@ -0,0 +1,23 @@ +# Use a base image with JDK and Maven installed +FROM maven:3.8.4-openjdk-17 AS build + +# Set the working directory in the container +WORKDIR /app + +# Copy the project files into the container +COPY . . + +# Build the application +RUN mvn clean package -DskipTests + +# Use a smaller base image for runtime +FROM openjdk:17-jdk-slim + +# Set the working directory in the container +WORKDIR /app + +# Copy the JAR file from the build stage into the container +COPY --from=build /app/target/transaction-0.0.1-SNAPSHOT.jar app.jar + +# Specify the command to run on container start +CMD ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/transaction/mvnw b/transaction/mvnw new file mode 100644 index 0000000..ba9212a --- /dev/null +++ b/transaction/mvnw @@ -0,0 +1,250 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.1 +# +# 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 +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl="${value-}" ;; + distributionSha256Sum) distributionSha256Sum="${value-}" ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_HOME="$HOME/.m2/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/transaction/mvnw.cmd b/transaction/mvnw.cmd new file mode 100644 index 0000000..406932d --- /dev/null +++ b/transaction/mvnw.cmd @@ -0,0 +1,146 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.1 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/transaction/pom.xml b/transaction/pom.xml new file mode 100644 index 0000000..41fed3a --- /dev/null +++ b/transaction/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.0 + + + com.yape + transaction + 0.0.1-SNAPSHOT + Code Challenge Transaction Service + Transaction service for the Yape Code Challenge + + 17 + + + + org.springframework.boot + spring-boot-starter-graphql + + + com.graphql-java-kickstart + graphql-java-tools + 11.1.0 + + + org.springframework.graphql + spring-graphql + + + com.fasterxml.jackson.core + jackson-databind + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.kafka + spring-kafka + + + org.apache.kafka + kafka-clients + + + jakarta.validation + jakarta.validation-api + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-docker-compose + runtime + true + + + org.postgresql + postgresql + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + com.graphql-java + graphql-java-extended-scalars + 22.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/transaction/src/main/java/com/yape/transaction/CodeChallengeTransactionServiceApplication.java b/transaction/src/main/java/com/yape/transaction/CodeChallengeTransactionServiceApplication.java new file mode 100644 index 0000000..931e1c8 --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/CodeChallengeTransactionServiceApplication.java @@ -0,0 +1,13 @@ +package com.yape.transaction; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CodeChallengeTransactionServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(CodeChallengeTransactionServiceApplication.class, args); + } + +} diff --git a/transaction/src/main/java/com/yape/transaction/config/AppConfig.java b/transaction/src/main/java/com/yape/transaction/config/AppConfig.java new file mode 100644 index 0000000..20274ce --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/config/AppConfig.java @@ -0,0 +1,14 @@ +package com.yape.transaction.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class AppConfig { + + @Bean + public RestTemplate customRestTemplate() { + return new RestTemplate(); + } +} diff --git a/transaction/src/main/java/com/yape/transaction/config/GraphQlConfig.java b/transaction/src/main/java/com/yape/transaction/config/GraphQlConfig.java new file mode 100644 index 0000000..4a0d19d --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/config/GraphQlConfig.java @@ -0,0 +1,17 @@ +package com.yape.transaction.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; + +import graphql.scalars.ExtendedScalars; + +@Configuration +public class GraphQlConfig { + + @Bean + public RuntimeWiringConfigurer runtimeWiringConfigurer() { + return wiringBuilder -> wiringBuilder.scalar(ExtendedScalars.GraphQLBigDecimal).scalar(ExtendedScalars.UUID); + + } +} diff --git a/transaction/src/main/java/com/yape/transaction/config/KafkaConfig.java b/transaction/src/main/java/com/yape/transaction/config/KafkaConfig.java new file mode 100644 index 0000000..7d0dbe5 --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/config/KafkaConfig.java @@ -0,0 +1,77 @@ +package com.yape.transaction.config; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +/** +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer; +import org.springframework.kafka.support.serializer.JsonDeserializer; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import com.yape.transaction.producers.YapeTransactionEventProducer; +**/ + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@EnableKafka +public class KafkaConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Value("${spring.kafka.consumer.group-id}") + private String groupId; + + @Bean + public ProducerFactory producerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } + + @Bean + public Map consumerConfigs() { + Map props = new HashMap<>(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "transaction-service-group"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + return props; + } + + @Bean + public ConsumerFactory consumerFactory() { + return new DefaultKafkaConsumerFactory<>(consumerConfigs()); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + return factory; + } +} diff --git a/transaction/src/main/java/com/yape/transaction/dtos/YapeTransactionInsertion.java b/transaction/src/main/java/com/yape/transaction/dtos/YapeTransactionInsertion.java new file mode 100644 index 0000000..805276b --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/dtos/YapeTransactionInsertion.java @@ -0,0 +1,21 @@ +package com.yape.transaction.dtos; + +import java.math.BigDecimal; +import java.util.UUID; + +import lombok.Data; + +@Data +public class YapeTransactionInsertion { + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private int transferTypeId; + private BigDecimal value; + + public YapeTransactionInsertion(UUID accountExternalIdDebit, UUID accountExternalIdCredit, int transferTypeId, BigDecimal value) { + this.accountExternalIdDebit = accountExternalIdDebit; + this.accountExternalIdCredit = accountExternalIdCredit; + this.transferTypeId = transferTypeId; + this.value = value; + } +} diff --git a/transaction/src/main/java/com/yape/transaction/dtos/YapeTransactionSingleRetrieval.java b/transaction/src/main/java/com/yape/transaction/dtos/YapeTransactionSingleRetrieval.java new file mode 100644 index 0000000..80fcda1 --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/dtos/YapeTransactionSingleRetrieval.java @@ -0,0 +1,33 @@ +package com.yape.transaction.dtos; + +import java.math.BigDecimal; +import java.util.UUID; + +import com.yape.transaction.entities.TransactionStatus; +import com.yape.transaction.entities.TransactionType; +import com.yape.transaction.entities.YapeTransaction; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class YapeTransactionSingleRetrieval { + private UUID transactionExternalId; + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private TransactionType transactionType; + private BigDecimal value; + private TransactionStatus transactionStatus; + private String createdAt; + + public YapeTransactionSingleRetrieval (YapeTransaction transaction) { + this.accountExternalIdDebit = transaction.getAccountExternalIdDebit(); + this.accountExternalIdCredit = transaction.getAccountExternalIdCredit(); + this.transactionExternalId = transaction.getTransactionExternalId(); + this.transactionType = transaction.getTransactionType(); + this.value = transaction.getValue(); + this.transactionStatus = transaction.getTransactionStatus(); + this.createdAt = transaction.getCreatedAt(); + } +} diff --git a/transaction/src/main/java/com/yape/transaction/entities/TransactionStatus.java b/transaction/src/main/java/com/yape/transaction/entities/TransactionStatus.java new file mode 100644 index 0000000..4254cc0 --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/entities/TransactionStatus.java @@ -0,0 +1,21 @@ +package com.yape.transaction.entities; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "transaction_statuses") +@Data +@NoArgsConstructor +public class TransactionStatus { + @Id + private int id; + + @Column(name = "name", nullable = false) + private String name; +} diff --git a/transaction/src/main/java/com/yape/transaction/entities/TransactionType.java b/transaction/src/main/java/com/yape/transaction/entities/TransactionType.java new file mode 100644 index 0000000..f0b3adf --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/entities/TransactionType.java @@ -0,0 +1,21 @@ +package com.yape.transaction.entities; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "transaction_types") +@Data +@NoArgsConstructor +public class TransactionType { + @Id + private int id; + + @Column(name = "name", nullable = false) + private String name; +} diff --git a/transaction/src/main/java/com/yape/transaction/entities/YapeTransaction.java b/transaction/src/main/java/com/yape/transaction/entities/YapeTransaction.java new file mode 100644 index 0000000..7c8854f --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/entities/YapeTransaction.java @@ -0,0 +1,48 @@ +package com.yape.transaction.entities; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.util.UUID; + +import jakarta.persistence.*; +import jakarta.validation.constraints.AssertTrue; + +@Entity +@Table(name = "transactions") +@Data +@NoArgsConstructor +@EqualsAndHashCode(of = "transactionExternalId") +public class YapeTransaction { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID transactionExternalId; + + @Column(name = "account_external_id_debit") + private UUID accountExternalIdDebit; + + @Column(name = "account_external_id_credit") + private UUID accountExternalIdCredit; + + @ManyToOne + @JoinColumn(name = "transfer_type_id", nullable = false) + private TransactionType transactionType; + + @Column(name = "value", nullable = false) + private BigDecimal value; + + @ManyToOne + @JoinColumn(name = "status_id", nullable = false) + private TransactionStatus transactionStatus; + + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_at", nullable = false) + private String createdAt; + + @AssertTrue(message = "Transaction must either come from debit or credit card") + private boolean isTransactionValid() { + return accountExternalIdDebit != null || accountExternalIdCredit != null; + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/yape/transaction/listeners/YapeTransactionListener.java b/transaction/src/main/java/com/yape/transaction/listeners/YapeTransactionListener.java new file mode 100644 index 0000000..7a1bcbd --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/listeners/YapeTransactionListener.java @@ -0,0 +1,50 @@ +package com.yape.transaction.listeners; + +import com.yape.transaction.entities.TransactionStatus; +import com.yape.transaction.entities.YapeTransaction; +import com.yape.transaction.repositories.TransactionStatusRepository; +import com.yape.transaction.repositories.YapeTransactionRepository; +import com.yape.transaction.services.YapeTransactionService; + +import java.util.Optional; +import java.util.UUID; + +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Service; + +@Service +public class YapeTransactionListener { + + private final YapeTransactionService transactionService; + + @Autowired + private YapeTransactionRepository transactionRepository; + + @Autowired + private TransactionStatusRepository statusRepository; + + @Autowired + public YapeTransactionListener(YapeTransactionService transactionService) { + this.transactionService = transactionService; + } + + @KafkaListener(topics = "validated-transactions", groupId = "${spring.kafka.consumer.group-id}") + public void listen(ConsumerRecord record) { + String transactionExternalId = record.key(); + int transactionStatus = Integer.parseInt(record.value()); + + Optional transaction = transactionRepository.findById(UUID.fromString(transactionExternalId)); + Optional status = statusRepository.findById(transactionStatus); + + transaction.ifPresent(existingTransaction ->{ + status.ifPresent(existingStatus->{ + existingTransaction.setTransactionStatus(existingStatus); + transactionRepository.save(existingTransaction); + transactionService.handleKafkaResponse(existingTransaction.getTransactionExternalId(), existingTransaction); + + }); + }); + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/yape/transaction/repositories/TransactionStatusRepository.java b/transaction/src/main/java/com/yape/transaction/repositories/TransactionStatusRepository.java new file mode 100644 index 0000000..0bdbe84 --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/repositories/TransactionStatusRepository.java @@ -0,0 +1,7 @@ +package com.yape.transaction.repositories; + +import com.yape.transaction.entities.TransactionStatus; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TransactionStatusRepository extends JpaRepository { +} diff --git a/transaction/src/main/java/com/yape/transaction/repositories/TransactionTypeRepository.java b/transaction/src/main/java/com/yape/transaction/repositories/TransactionTypeRepository.java new file mode 100644 index 0000000..5008a7e --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/repositories/TransactionTypeRepository.java @@ -0,0 +1,7 @@ +package com.yape.transaction.repositories; + +import com.yape.transaction.entities.TransactionType; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TransactionTypeRepository extends JpaRepository { +} diff --git a/transaction/src/main/java/com/yape/transaction/repositories/YapeTransactionRepository.java b/transaction/src/main/java/com/yape/transaction/repositories/YapeTransactionRepository.java new file mode 100644 index 0000000..1613a99 --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/repositories/YapeTransactionRepository.java @@ -0,0 +1,11 @@ +package com.yape.transaction.repositories; + +import org.springframework.data.jpa.repository.JpaRepository; + +import com.yape.transaction.entities.YapeTransaction; + +import java.util.UUID; + +public interface YapeTransactionRepository extends JpaRepository { + YapeTransaction findByTransactionExternalId(UUID transactionExternalId); +} \ No newline at end of file diff --git a/transaction/src/main/java/com/yape/transaction/resolvers/YapeTransactionResolver.java b/transaction/src/main/java/com/yape/transaction/resolvers/YapeTransactionResolver.java new file mode 100644 index 0000000..255f7e1 --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/resolvers/YapeTransactionResolver.java @@ -0,0 +1,37 @@ +package com.yape.transaction.resolvers; + +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; + +import com.yape.transaction.dtos.YapeTransactionInsertion; +import com.yape.transaction.dtos.YapeTransactionSingleRetrieval; +import com.yape.transaction.services.YapeTransactionService; + +import graphql.kickstart.tools.GraphQLMutationResolver; +import graphql.kickstart.tools.GraphQLQueryResolver; + +@Controller +public class YapeTransactionResolver implements GraphQLQueryResolver, GraphQLMutationResolver { + + @Autowired + private final YapeTransactionService transactionService; + + public YapeTransactionResolver(YapeTransactionService transactionService) { + this.transactionService = transactionService; + } + + @MutationMapping + public YapeTransactionSingleRetrieval createTransaction(@Argument YapeTransactionInsertion input) { + return transactionService.createTransaction(input); + } + + @QueryMapping + public YapeTransactionSingleRetrieval getTransaction(@Argument UUID transactionExternalId) { + return transactionService.getTransaction(transactionExternalId); + } +} \ No newline at end of file diff --git a/transaction/src/main/java/com/yape/transaction/services/YapeTransactionService.java b/transaction/src/main/java/com/yape/transaction/services/YapeTransactionService.java new file mode 100644 index 0000000..13e6bd6 --- /dev/null +++ b/transaction/src/main/java/com/yape/transaction/services/YapeTransactionService.java @@ -0,0 +1,98 @@ +package com.yape.transaction.services; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +import com.yape.transaction.dtos.YapeTransactionInsertion; +import com.yape.transaction.dtos.YapeTransactionSingleRetrieval; +import com.yape.transaction.entities.TransactionStatus; +import com.yape.transaction.entities.TransactionType; +import com.yape.transaction.entities.YapeTransaction; +import com.yape.transaction.repositories.TransactionStatusRepository; +import com.yape.transaction.repositories.TransactionTypeRepository; +import com.yape.transaction.repositories.YapeTransactionRepository; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + +@Service +public class YapeTransactionService { + + private final ConcurrentHashMap> responseFutures = new ConcurrentHashMap<>(); + + @Autowired + private YapeTransactionRepository transactionRepository; + + @Autowired + private TransactionTypeRepository transactionTypeRepository; + + @Autowired + private TransactionStatusRepository transactionStatusRepository; + + @Autowired + private KafkaTemplate kafkaTemplate; + + @Autowired + public YapeTransactionService(YapeTransactionRepository transactionRepository, KafkaTemplate kafkaTemplate) { + this.transactionRepository = transactionRepository; + this.kafkaTemplate = kafkaTemplate; + } + + public YapeTransactionSingleRetrieval createTransaction(YapeTransactionInsertion request) { + + Optional transactionType = transactionTypeRepository.findById(request.getTransferTypeId()); + if (!transactionType.isPresent()) { + throw new IllegalArgumentException("Invalid transaction type ID: " + request.getTransferTypeId()); + } + + Optional transactionStatus = transactionStatusRepository.findById(1); + if (!transactionStatus.isPresent()) { + throw new IllegalArgumentException("Invalid transaction status ID: 1"); + } + + YapeTransaction transaction = new YapeTransaction(); + CompletableFuture futureResponse = new CompletableFuture<>(); + + transaction.setAccountExternalIdDebit(request.getAccountExternalIdDebit()); + transaction.setAccountExternalIdCredit(request.getAccountExternalIdCredit()); + transaction.setTransactionType(transactionType.get()); + transaction.setValue(request.getValue()); + transaction.setTransactionStatus(transactionStatus.get()); + transaction.setCreatedAt(LocalDateTime.now()+""); + transactionRepository.save(transaction); + responseFutures.put(transaction.getTransactionExternalId(), futureResponse); + + kafkaTemplate.send("transaction-events", transaction.getTransactionExternalId()+"", transaction.getValue()+""); + + try { + return futureResponse.get(30, TimeUnit.SECONDS); + } catch (Exception e) { + throw new RuntimeException("Timeout or interruption while waiting for anti-fraud response", e); + } finally { + responseFutures.remove(transaction.getTransactionExternalId()); + } + + } + + public void handleKafkaResponse(UUID transactionId, YapeTransaction response) { + CompletableFuture future = responseFutures.remove(transactionId); + if (future != null) { + future.complete(new YapeTransactionSingleRetrieval(response)); + } + } + + public YapeTransactionSingleRetrieval getTransaction(UUID transactionExternalId) { + Optional transaction = transactionRepository.findById(transactionExternalId); + YapeTransactionSingleRetrieval response = new YapeTransactionSingleRetrieval(); + if(transaction.isPresent()) { + response = new YapeTransactionSingleRetrieval(transaction.get()); + } + return response; + } + +} diff --git a/transaction/src/main/resources/application.properties b/transaction/src/main/resources/application.properties new file mode 100644 index 0000000..2ca0618 --- /dev/null +++ b/transaction/src/main/resources/application.properties @@ -0,0 +1,40 @@ +spring.application.name=Code Challenge Transaction Service + +# Database Configuration +spring.datasource.url=jdbc:postgresql://postgres:5432/mydatabase +spring.datasource.username=myuser +spring.datasource.password=secret +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.hibernate.ddl-auto=update +spring.datasource.initialization-mode=always +spring.jpa.defer-datasource-initialization=true +spring.sql.init.mode=always + +# Hibernate properties +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true + +# Logging +logging.level.org.hibernate.SQL=DEBUG +logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE + +# URLs and etc +anti-fraud-service.url=http://localhost:8081/anti-fraud + +# Kafka Config +spring.kafka.bootstrap-servers=kafka:9092 +spring.kafka.consumer.auto-offset-reset=earliest +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.group-id=transaction-service-group +spring.kafka.topic.transaction-events=transaction-events +spring.kafka.topic.validated-transactions=validated-transactions + +# GraphQL Config +spring.graphql.servlet.mapping=/graphql +spring.graphql.servlet.enabled=true +spring.graphql.path=/graphql +logging.level.org.springframework.boot.autoconfigure.graphql=DEBUG +logging.level.graphql=DEBUG \ No newline at end of file diff --git a/transaction/src/main/resources/data.sql b/transaction/src/main/resources/data.sql new file mode 100644 index 0000000..11f33b5 --- /dev/null +++ b/transaction/src/main/resources/data.sql @@ -0,0 +1,9 @@ +-- Insert Transaction Types +INSERT INTO transaction_types (id, name) VALUES (1, 'Outbound Transfer'); +INSERT INTO transaction_types (id, name) VALUES (2, 'Inbound Transfer'); +INSERT INTO transaction_types (id, name) VALUES (3, 'Withdrawal'); + +-- Insert Transaction Statuses +INSERT INTO transaction_statuses (id, name) VALUES (1, 'PENDING'); +INSERT INTO transaction_statuses (id, name) VALUES (2, 'APPROVED'); +INSERT INTO transaction_statuses (id, name) VALUES (3, 'REJECTED'); diff --git a/transaction/src/main/resources/graphql/schema.graphqls b/transaction/src/main/resources/graphql/schema.graphqls new file mode 100644 index 0000000..8cf2c35 --- /dev/null +++ b/transaction/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,35 @@ +scalar BigDecimal +scalar UUID + +type YapeTransactionSingleRetrieval { + transactionExternalId: UUID + accountExternalIdDebit: UUID + accountExternalIdCredit: UUID + transactionType: TransactionType + value: BigDecimal + transactionStatus: TransactionStatus + createdAt: String +} + +input YapeTransactionInsertion { + accountExternalIdDebit: UUID + accountExternalIdCredit: UUID + transferTypeId: Int! + value: BigDecimal! +} + +type TransactionStatus { + name: String +} + +type TransactionType { + name: String +} + +type Query { + getTransaction(transactionExternalId: UUID!): YapeTransactionSingleRetrieval +} + +type Mutation { + createTransaction(input: YapeTransactionInsertion!): YapeTransactionSingleRetrieval +} \ No newline at end of file diff --git a/transaction/src/main/webapp/META-INF/MANIFEST.MF b/transaction/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 0000000..254272e --- /dev/null +++ b/transaction/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/transaction/src/test/java/com/yape/transaction/CodeChallengeTransactionServiceApplicationTests.java b/transaction/src/test/java/com/yape/transaction/CodeChallengeTransactionServiceApplicationTests.java new file mode 100644 index 0000000..e8a27e1 --- /dev/null +++ b/transaction/src/test/java/com/yape/transaction/CodeChallengeTransactionServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.yape.transaction; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class CodeChallengeTransactionServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/yape-challenge-environment.yml b/yape-challenge-environment.yml new file mode 100644 index 0000000..c9f3431 --- /dev/null +++ b/yape-challenge-environment.yml @@ -0,0 +1,83 @@ +version: '3.8' + +services: + + zookeeper: + image: confluentinc/cp-zookeeper:6.2.0 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - 2181:2181 + networks: + - transaction-network + + kafka: + image: confluentinc/cp-kafka:6.2.0 + container_name: kafka + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092 + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT + KAFKA_AUTO_CREATE_TOPICS_ENABLE: 'true' + KAFKA_CREATE_TOPICS: "transaction-events:1:1,validated-transactions:1:1" + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9991 + ports: + - "9092:9092" + - "29092:29092" + depends_on: + - zookeeper + networks: + - transaction-network + + postgres: + image: postgres:latest + container_name: postgres + environment: + POSTGRES_DB: mydatabase + POSTGRES_USER: myuser + POSTGRES_PASSWORD: secret + ports: + - "5432:5432" + networks: + - transaction-network + + transaction-service: + build: + context: ./transaction + container_name: transaction-service + ports: + - "8080:8080" + depends_on: + - postgres + - kafka + environment: + SPRING_PROFILES_ACTIVE: docker + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mydatabase + SPRING_DATASOURCE_USERNAME: myuser + SPRING_DATASOURCE_PASSWORD: secret + SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092 + SPRING_KAFKA_CONSUMER_GROUP_ID: transaction-service-group + networks: + - transaction-network + + anti-fraud-service: + build: + context: ./antifraud + container_name: anti-fraud-service + ports: + - "8081:8081" + depends_on: + - kafka + environment: + SPRING_PROFILES_ACTIVE: docker + SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:9092 + SPRING_KAFKA_CONSUMER_GROUP_ID: transaction-service-group + networks: + - transaction-network + +networks: + transaction-network: + driver: bridge