From ff0232ad8636166b509bca7db5faa73c00f48ee4 Mon Sep 17 00:00:00 2001 From: Luis Gutierrez Date: Fri, 30 Jan 2026 13:07:54 -0500 Subject: [PATCH 1/2] first commit --- .idea/compiler.xml | 23 ++ .idea/copilot.data.migration.agent.xml | 6 + .idea/copilot.data.migration.ask.xml | 6 + .idea/copilot.data.migration.ask2agent.xml | 6 + .idea/copilot.data.migration.edit.xml | 6 + .idea/encodings.xml | 7 + .idea/jarRepositories.xml | 20 ++ .idea/misc.xml | 15 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + .idea/workspace.xml | 185 +++++++++++ antifraud-service/.gitattributes | 2 + antifraud-service/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.properties | 3 + antifraud-service/mvnw | 295 ++++++++++++++++++ antifraud-service/mvnw.cmd | 189 +++++++++++ antifraud-service/pom.xml | 116 +++++++ .../AntifraudServiceApplication.java | 16 + .../config/KafkaProducerConfig.java | 31 ++ .../config/KafkaTopicConfig.java | 20 ++ .../controller/AntifraudController.java | 30 ++ .../antifraudservice/kafka/EventProducer.java | 40 +++ .../src/main/resources/application.yml | 30 ++ .../AntifraudServiceApplicationTests.java | 13 + docker-compose.yml | 37 ++- transaction-service/.gitattributes | 2 + transaction-service/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.properties | 3 + transaction-service/mvnw | 295 ++++++++++++++++++ transaction-service/mvnw.cmd | 189 +++++++++++ transaction-service/pom.xml | 130 ++++++++ .../TransactionServiceApplication.java | 13 + .../in/kafka/TransactionStatusConsumer.java | 25 ++ .../adapter/in/web/AccountController.java | 30 ++ .../adapter/in/web/TransactionController.java | 37 +++ .../adapter/in/web/dto/AccountRequest.java | 10 + .../adapter/in/web/dto/AccountResponse.java | 12 + .../in/web/dto/TransactionRequest.java | 13 + .../in/web/dto/TransactionResponse.java | 17 + .../in/web/mapper/AccountWebMapper.java | 32 ++ .../in/web/mapper/TransactionWebMapper.java | 38 +++ .../out/client/AntiFraudWebClientAdapter.java | 26 ++ .../out/client/FraudScoringWebClient.java | 4 + .../client/adapter/FraudScoringAdapter.java | 4 + .../out/client/dto/FraudScoreRequest.java | 4 + .../out/client/dto/FraudScoreResponse.java | 4 + .../adapter/AccountRepositoryAdapter.java | 34 ++ .../adapter/TransactionRepositoryAdapter.java | 32 ++ .../out/persistence/entity/AccountEntity.java | 36 +++ .../persistence/entity/TransactionEntity.java | 40 +++ .../mapper/AccountPersistenceMapper.java | 30 ++ .../mapper/TransactionPersistenceMapper.java | 33 ++ .../repository/R2dbcAccountRepository.java | 9 + .../R2dbcTransactionRepository.java | 9 + .../dto/command/CreateAccountCommand.java | 9 + .../dto/command/CreateTransactionCommand.java | 10 + .../application/dto/result/AccountResult.java | 12 + .../dto/result/TransactionResult.java | 18 ++ .../mapper/AccountApplicationMapper.java | 18 ++ .../port/in/CreateAccountUseCase.java | 9 + .../port/in/CreateTransactionUseCase.java | 11 + .../port/in/GetAccountUseCase.java | 4 + .../port/in/GetTransactionUseCase.java | 10 + .../port/in/ListTransactionsUseCase.java | 4 + .../in/UpdateTransactionStatusUseCase.java | 9 + .../port/out/AccountRepositoryPort.java | 14 + .../application/port/out/AntiFraudPort.java | 10 + .../port/out/TransactionRepositoryPort.java | 14 + .../usecase/CreateAccountService.java | 42 +++ .../usecase/CreateTransactionService.java | 85 +++++ .../usecase/GetAccountService.java | 4 + .../usecase/GetTransactionService.java | 35 +++ .../usecase/ListTransactionsService.java | 4 + .../UpdateTransactionStatusService.java | 33 ++ .../domain/enums/AccountStatus.java | 6 + .../domain/enums/AccountType.java | 6 + .../domain/enums/TransactionStatus.java | 7 + .../domain/enums/TransactionType.java | 8 + .../domain/exception/APIException.java | 16 + .../domain/exception/BadRequestException.java | 8 + .../exception/ResourceNotFoundException.java | 21 ++ .../domain/model/Account.java | 80 +++++ .../domain/model/Transaction.java | 48 +++ .../domain/service/FraudPolicy.java | 4 + .../infrastructure/config/OpenApiConfig.java | 4 + .../config/PersistenceConfig.java | 114 +++++++ .../config/WebClientConfig.java | 26 ++ .../exception/ApiExceptionMapper.java | 83 +++++ .../exception/ErrorDetails.java | 77 +++++ .../exception/GlobalExceptionHandler.java | 47 +++ .../src/main/resources/application.yml | 40 +++ .../TransactionServiceApplicationTests.java | 13 + 92 files changed, 3235 insertions(+), 15 deletions(-) create mode 100644 .idea/compiler.xml create mode 100644 .idea/copilot.data.migration.agent.xml create mode 100644 .idea/copilot.data.migration.ask.xml create mode 100644 .idea/copilot.data.migration.ask2agent.xml create mode 100644 .idea/copilot.data.migration.edit.xml create mode 100644 .idea/encodings.xml create mode 100644 .idea/jarRepositories.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml create mode 100644 antifraud-service/.gitattributes create mode 100644 antifraud-service/.gitignore create mode 100644 antifraud-service/.mvn/wrapper/maven-wrapper.properties create mode 100755 antifraud-service/mvnw create mode 100644 antifraud-service/mvnw.cmd create mode 100644 antifraud-service/pom.xml create mode 100644 antifraud-service/src/main/java/com/devpull/antifraudservice/AntifraudServiceApplication.java create mode 100644 antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaProducerConfig.java create mode 100644 antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaTopicConfig.java create mode 100644 antifraud-service/src/main/java/com/devpull/antifraudservice/controller/AntifraudController.java create mode 100644 antifraud-service/src/main/java/com/devpull/antifraudservice/kafka/EventProducer.java create mode 100644 antifraud-service/src/main/resources/application.yml create mode 100644 antifraud-service/src/test/java/com/devpull/antifraudservice/AntifraudServiceApplicationTests.java create mode 100644 transaction-service/.gitattributes create mode 100644 transaction-service/.gitignore create mode 100644 transaction-service/.mvn/wrapper/maven-wrapper.properties create mode 100755 transaction-service/mvnw create mode 100644 transaction-service/mvnw.cmd create mode 100644 transaction-service/pom.xml create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/TransactionServiceApplication.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/TransactionStatusConsumer.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/AccountController.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/TransactionController.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/AccountRequest.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/AccountResponse.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/TransactionRequest.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/TransactionResponse.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/mapper/AccountWebMapper.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/mapper/TransactionWebMapper.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/AntiFraudWebClientAdapter.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/FraudScoringWebClient.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/adapter/FraudScoringAdapter.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/FraudScoreRequest.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/FraudScoreResponse.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/AccountRepositoryAdapter.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/TransactionRepositoryAdapter.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/AccountEntity.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/TransactionEntity.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/AccountPersistenceMapper.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/TransactionPersistenceMapper.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/repository/R2dbcAccountRepository.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/repository/R2dbcTransactionRepository.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/dto/command/CreateAccountCommand.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/dto/command/CreateTransactionCommand.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/dto/result/AccountResult.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/dto/result/TransactionResult.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/mapper/AccountApplicationMapper.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/CreateAccountUseCase.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/CreateTransactionUseCase.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/GetAccountUseCase.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/GetTransactionUseCase.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/ListTransactionsUseCase.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/UpdateTransactionStatusUseCase.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/AccountRepositoryPort.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/AntiFraudPort.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/TransactionRepositoryPort.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateAccountService.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateTransactionService.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/GetAccountService.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/GetTransactionService.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/ListTransactionsService.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/UpdateTransactionStatusService.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/AccountStatus.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/AccountType.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionStatus.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionType.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/APIException.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/BadRequestException.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/ResourceNotFoundException.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/domain/model/Account.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/domain/model/Transaction.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/domain/service/FraudPolicy.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/OpenApiConfig.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/PersistenceConfig.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/WebClientConfig.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/ApiExceptionMapper.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/ErrorDetails.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/GlobalExceptionHandler.java create mode 100644 transaction-service/src/main/resources/application.yml create mode 100644 transaction-service/src/test/java/com/devpull/transactionservice/TransactionServiceApplicationTests.java diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000000..bd923295ad --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000000..4ea72a911a --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml new file mode 100644 index 0000000000..7ef04e2ea0 --- /dev/null +++ b/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ 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/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000000..8648f9401a --- /dev/null +++ b/.idea/copilot.data.migration.edit.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..7cf716e071 --- /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..712ab9d985 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..621aeb23b9 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000..5e7c38b78c --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..35eb1ddfbb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 0000000000..4144e315f3 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,185 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + "lastFilter": { + "state": "OPEN", + "assignee": "3nr19u3" + } +} + { + "selectedUrlAndAccountId": { + "url": "git@github.com:3nr19u3/app-java-codechallenge.git", + "accountId": "ccb41516-fc70-4471-9fb5-9b6453f6b0eb" + } +} + { + "associatedIndex": 1 +} + + + + + + + + + + + + + + + + + + + + 1769557517634 + + + + + + + + + + + + \ No newline at end of file diff --git a/antifraud-service/.gitattributes b/antifraud-service/.gitattributes new file mode 100644 index 0000000000..3b41682ac5 --- /dev/null +++ b/antifraud-service/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/antifraud-service/.gitignore b/antifraud-service/.gitignore new file mode 100644 index 0000000000..667aaef0c8 --- /dev/null +++ b/antifraud-service/.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-service/.mvn/wrapper/maven-wrapper.properties b/antifraud-service/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..8dea6c227c --- /dev/null +++ b/antifraud-service/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/antifraud-service/mvnw b/antifraud-service/mvnw new file mode 100755 index 0000000000..bd8896bf22 --- /dev/null +++ b/antifraud-service/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/antifraud-service/mvnw.cmd b/antifraud-service/mvnw.cmd new file mode 100644 index 0000000000..92450f9327 --- /dev/null +++ b/antifraud-service/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/antifraud-service/pom.xml b/antifraud-service/pom.xml new file mode 100644 index 0000000000..98e82d2028 --- /dev/null +++ b/antifraud-service/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.2 + + + com.devpull + antifraud-service + 0.0.1-SNAPSHOT + antifraud-service + antifraud-service + + + + + + + devPull + luis.dev.pull@gmail.com + Luis Gutierrez + devPull + + developer + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.kafka + spring-kafka + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-webflux-test + test + + + com.devpull + transaction-service + 0.0.1-SNAPSHOT + + + org.springframework.kafka + spring-kafka + + + com.fasterxml.jackson.core + jackson-databind + + + org.springframework.boot + spring-boot-starter-actuator + 4.1.0-M1 + compile + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/AntifraudServiceApplication.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/AntifraudServiceApplication.java new file mode 100644 index 0000000000..576652b6e3 --- /dev/null +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/AntifraudServiceApplication.java @@ -0,0 +1,16 @@ +package com.devpull.antifraudservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.r2dbc.autoconfigure.R2dbcAutoConfiguration; + +@SpringBootApplication(exclude = { + R2dbcAutoConfiguration.class // Exclude R2DBC auto-configuration +}) +public class AntifraudServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(AntifraudServiceApplication.class, args); + } + +} diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaProducerConfig.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaProducerConfig.java new file mode 100644 index 0000000000..d038a174bd --- /dev/null +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaProducerConfig.java @@ -0,0 +1,31 @@ +package com.devpull.antifraudservice.config; + +import com.fasterxml.jackson.databind.ser.std.StringSerializer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.ByteArraySerializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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 +public class KafkaProducerConfig { + + @Bean + public ProducerFactory producerFactory() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); + return new DefaultKafkaProducerFactory<>(props); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } +} diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaTopicConfig.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaTopicConfig.java new file mode 100644 index 0000000000..2a77ae9422 --- /dev/null +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaTopicConfig.java @@ -0,0 +1,20 @@ +package com.devpull.antifraudservice.config; + +import org.apache.kafka.clients.admin.NewTopic; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.TopicBuilder; + +@Configuration +public class KafkaTopicConfig { + + @Value("${spring.kafka.topic.name}") + private String topicName; + + //create a bean for kafka topic + @Bean + public NewTopic getTopic() { + return TopicBuilder.name(topicName).build(); + } +} diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/controller/AntifraudController.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/controller/AntifraudController.java new file mode 100644 index 0000000000..06959bb97d --- /dev/null +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/controller/AntifraudController.java @@ -0,0 +1,30 @@ +package com.devpull.antifraudservice.controller; + +import com.devpull.antifraudservice.kafka.EventProducer; +import com.devpull.transactionservice.domain.enums.TransactionStatus; +import com.devpull.transactionservice.domain.model.Transaction; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequestMapping("/api/v1/antifraud") +public class AntifraudController { + + private final EventProducer eventProducer; + + public AntifraudController(EventProducer eventProducer){ + this.eventProducer = eventProducer; + } + + @PostMapping + public String evaluateAntifraud(@RequestBody Transaction transaction) throws Exception { + String msj = transaction.getAmount() > 1000 ? "REJECT" : "APPROVED"; + transaction.setStatus(TransactionStatus.valueOf(msj)); + eventProducer.sendMessage(transaction); + + return "Event handled successfully!"; + } +} diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/kafka/EventProducer.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/kafka/EventProducer.java new file mode 100644 index 0000000000..9bb1221abe --- /dev/null +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/kafka/EventProducer.java @@ -0,0 +1,40 @@ +package com.devpull.antifraudservice.kafka; + +import com.devpull.transactionservice.domain.model.Transaction; +import lombok.extern.slf4j.Slf4j; +import org.apache.kafka.clients.admin.NewTopic; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.KafkaHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Service; +import tools.jackson.databind.ObjectMapper; + +@Service +@Slf4j +public class EventProducer { + + private final KafkaTemplate kafkaTemplate; + private final ObjectMapper objectMapper; + private final NewTopic topic; + + public EventProducer( + KafkaTemplate kafkaTemplate, + ObjectMapper objectMapper, + NewTopic topic + ) { + this.kafkaTemplate = kafkaTemplate; + this.objectMapper = objectMapper; + this.topic = topic; + } + + public void sendMessage(Transaction transaction) { + try { + byte[] payload = objectMapper.writeValueAsBytes(transaction); + kafkaTemplate.send(topic.name(), payload); + } catch (Exception e) { + log.error("Error serializing transaction event", e); + } + } + +} diff --git a/antifraud-service/src/main/resources/application.yml b/antifraud-service/src/main/resources/application.yml new file mode 100644 index 0000000000..3cd584726d --- /dev/null +++ b/antifraud-service/src/main/resources/application.yml @@ -0,0 +1,30 @@ +server: + port: ${SERVER_PORT:8081} + +spring: + application: + name: antifraud-service + + main: + web-application-type: reactive + + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} + + consumer: + group-id: ${KAFKA_CONSUMER_GROUP:antifraud-service} + auto-offset-reset: earliest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring: + json: + trusted: + packages: "*" + + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + + topic: + name: ${KAFKA_TOPIC_TRANSACTIONS:transactions_fraud} diff --git a/antifraud-service/src/test/java/com/devpull/antifraudservice/AntifraudServiceApplicationTests.java b/antifraud-service/src/test/java/com/devpull/antifraudservice/AntifraudServiceApplicationTests.java new file mode 100644 index 0000000000..5ef95f2603 --- /dev/null +++ b/antifraud-service/src/test/java/com/devpull/antifraudservice/AntifraudServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.devpull.antifraudservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AntifraudServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/docker-compose.yml b/docker-compose.yml index 0e8807f21c..0e32a0418d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,32 @@ -version: "3.7" services: postgres: - image: postgres:14 - ports: - - "5432:5432" + image: postgres:16 + container_name: postgres environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres + POSTGRES_DB: app_java_codechallenge + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5433:5432" zookeeper: - image: confluentinc/cp-zookeeper:5.5.3 + image: confluentinc/cp-zookeeper:7.5.0 + container_name: zookeeper environment: ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - "2181:2181" + kafka: - image: confluentinc/cp-enterprise-kafka:5.5.3 - depends_on: [zookeeper] + image: confluentinc/cp-kafka:7.5.0 + container_name: kafka + depends_on: + - zookeeper + ports: + - "9092:9092" environment: - KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092 + KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_JMX_PORT: 9991 - ports: - - 9092:9092 diff --git a/transaction-service/.gitattributes b/transaction-service/.gitattributes new file mode 100644 index 0000000000..3b41682ac5 --- /dev/null +++ b/transaction-service/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/transaction-service/.gitignore b/transaction-service/.gitignore new file mode 100644 index 0000000000..667aaef0c8 --- /dev/null +++ b/transaction-service/.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-service/.mvn/wrapper/maven-wrapper.properties b/transaction-service/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..8dea6c227c --- /dev/null +++ b/transaction-service/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/transaction-service/mvnw b/transaction-service/mvnw new file mode 100755 index 0000000000..bd8896bf22 --- /dev/null +++ b/transaction-service/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/transaction-service/mvnw.cmd b/transaction-service/mvnw.cmd new file mode 100644 index 0000000000..92450f9327 --- /dev/null +++ b/transaction-service/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/transaction-service/pom.xml b/transaction-service/pom.xml new file mode 100644 index 0000000000..d3d9708414 --- /dev/null +++ b/transaction-service/pom.xml @@ -0,0 +1,130 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.2 + + + com.devpull + transaction-service + 0.0.1-SNAPSHOT + transaction-service + transaction-service + + + + + + + devPull + luis.dev.pull@gmail.com + Luis Gutierrez + devPull + + developer + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-data-r2dbc + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.postgresql + postgresql + runtime + + + org.postgresql + r2dbc-postgresql + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-data-r2dbc-test + test + + + org.springframework.boot + spring-boot-starter-webclient + 4.1.0-M1 + compile + + + jakarta.validation + jakarta.validation-api + 4.0.0-M1 + compile + + + org.springframework.kafka + spring-kafka + 4.0.2 + compile + + + org.springframework.boot + spring-boot-starter-actuator + 4.1.0-M1 + compile + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/TransactionServiceApplication.java b/transaction-service/src/main/java/com/devpull/transactionservice/TransactionServiceApplication.java new file mode 100644 index 0000000000..abe5dc3659 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/TransactionServiceApplication.java @@ -0,0 +1,13 @@ +package com.devpull.transactionservice; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TransactionServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(TransactionServiceApplication.class, args); + } + +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/TransactionStatusConsumer.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/TransactionStatusConsumer.java new file mode 100644 index 0000000000..d131ef4ace --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/TransactionStatusConsumer.java @@ -0,0 +1,25 @@ +package com.devpull.transactionservice.adapter.in.kafka; + +import com.devpull.transactionservice.application.port.in.UpdateTransactionStatusUseCase; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Component +public class TransactionStatusConsumer { + + private final UpdateTransactionStatusUseCase useCase; + + public TransactionStatusConsumer(UpdateTransactionStatusUseCase useCase) { + this.useCase = useCase; + } + + @KafkaListener(topics = "${spring.kafka.topic.name}", groupId = "${spring.kafka.consumer.group-id}") + public void consume(TransactionStatusMessage message) { + useCase.updateStatus(message.transactionId(), message.status()) + .subscribe(); // subscribe to trigger the execution + } + + public record TransactionStatusMessage(UUID transactionId, String status) {} +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/AccountController.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/AccountController.java new file mode 100644 index 0000000000..fa0bb28f52 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/AccountController.java @@ -0,0 +1,30 @@ +package com.devpull.transactionservice.adapter.in.web; + +import com.devpull.transactionservice.adapter.in.web.dto.AccountRequest; +import com.devpull.transactionservice.adapter.in.web.dto.AccountResponse; +import com.devpull.transactionservice.adapter.in.web.mapper.AccountWebMapper; +import com.devpull.transactionservice.application.port.in.CreateAccountUseCase; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@AllArgsConstructor +@RestController +@RequestMapping("/api/account") +public class AccountController { + + private final CreateAccountUseCase createAccountUseCase; + + + @PostMapping + public Mono> createAccount(@RequestBody AccountRequest request) { + return createAccountUseCase.createAccount(AccountWebMapper.toCommand(request)) + .map(AccountWebMapper::toResponse) + .map(res -> ResponseEntity.status(HttpStatus.CREATED).body(res)); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/TransactionController.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/TransactionController.java new file mode 100644 index 0000000000..b9d6a9e9a4 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/TransactionController.java @@ -0,0 +1,37 @@ +package com.devpull.transactionservice.adapter.in.web; + +import com.devpull.transactionservice.adapter.in.web.dto.TransactionRequest; +import com.devpull.transactionservice.adapter.in.web.dto.TransactionResponse; +import com.devpull.transactionservice.adapter.in.web.mapper.TransactionWebMapper; +import com.devpull.transactionservice.application.port.in.CreateTransactionUseCase; +import com.devpull.transactionservice.application.port.in.GetTransactionUseCase; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +@AllArgsConstructor +@RestController +@RequestMapping("/api/transactions") +public class TransactionController { + + private final GetTransactionUseCase getTransactionUseCase; + private final CreateTransactionUseCase createTransactionUseCase; + + @GetMapping("{id}") + public Mono> getTransaction(@PathVariable("id") UUID transactionId) { + return getTransactionUseCase.getById(transactionId) + .map(TransactionWebMapper::toResponse) + .map(ResponseEntity::ok); + } + + @PostMapping + public Mono> createTransaction(@RequestBody TransactionRequest request) { + return createTransactionUseCase.create(TransactionWebMapper.toCommand(request)) + .map(TransactionWebMapper::toResponse) + .map(res -> ResponseEntity.status(HttpStatus.CREATED).body(res)); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/AccountRequest.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/AccountRequest.java new file mode 100644 index 0000000000..a47bc000a0 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/AccountRequest.java @@ -0,0 +1,10 @@ +package com.devpull.transactionservice.adapter.in.web.dto; + +import com.devpull.transactionservice.domain.enums.AccountStatus; +import com.devpull.transactionservice.domain.enums.AccountType; +import jakarta.validation.constraints.NotNull; + +public record AccountRequest( + @NotNull AccountStatus accountStatus, + @NotNull AccountType accountType +) {} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/AccountResponse.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/AccountResponse.java new file mode 100644 index 0000000000..9128afefe0 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/AccountResponse.java @@ -0,0 +1,12 @@ +package com.devpull.transactionservice.adapter.in.web.dto; + +import com.devpull.transactionservice.domain.enums.AccountStatus; +import com.devpull.transactionservice.domain.enums.AccountType; + +import java.util.UUID; + +public record AccountResponse( + UUID id, + AccountStatus accountStatus, + AccountType accountType +) {} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/TransactionRequest.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/TransactionRequest.java new file mode 100644 index 0000000000..c697d6a752 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/TransactionRequest.java @@ -0,0 +1,13 @@ +package com.devpull.transactionservice.adapter.in.web.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.util.UUID; + +public record TransactionRequest( + @NotNull UUID accountExternalIdDebit, + @NotNull UUID accountExternalIdCredit, + @NotBlank String transferTypeId, + @NotNull Double value +) {} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/TransactionResponse.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/TransactionResponse.java new file mode 100644 index 0000000000..46eca7f011 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/TransactionResponse.java @@ -0,0 +1,17 @@ +package com.devpull.transactionservice.adapter.in.web.dto; + +import com.devpull.transactionservice.domain.enums.TransactionStatus; +import com.devpull.transactionservice.domain.enums.TransactionType; + +import java.time.Instant; +import java.util.UUID; + +public record TransactionResponse( + UUID id, + TransactionStatus status, + TransactionType type, + double amount, + Instant createdAt, + Instant updatedAt, + UUID accountId +) {} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/mapper/AccountWebMapper.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/mapper/AccountWebMapper.java new file mode 100644 index 0000000000..9af210ef3f --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/mapper/AccountWebMapper.java @@ -0,0 +1,32 @@ +package com.devpull.transactionservice.adapter.in.web.mapper; + +import com.devpull.transactionservice.adapter.in.web.dto.AccountRequest; +import com.devpull.transactionservice.adapter.in.web.dto.AccountResponse; +import com.devpull.transactionservice.application.dto.command.CreateAccountCommand; +import com.devpull.transactionservice.application.dto.result.AccountResult; + +public final class AccountWebMapper { + + private AccountWebMapper() {} + + /** + * Web -> Application + */ + public static CreateAccountCommand toCommand(AccountRequest request) { + return new CreateAccountCommand( + request.accountStatus(), + request.accountType() + ); + } + + /** + * Application -> Web + */ + public static AccountResponse toResponse(AccountResult result) { + return new AccountResponse( + result.id(), + result.accountStatus(), + result.accountType() + ); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/mapper/TransactionWebMapper.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/mapper/TransactionWebMapper.java new file mode 100644 index 0000000000..713903ed43 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/mapper/TransactionWebMapper.java @@ -0,0 +1,38 @@ +package com.devpull.transactionservice.adapter.in.web.mapper; + +import com.devpull.transactionservice.adapter.in.web.dto.TransactionRequest; +import com.devpull.transactionservice.adapter.in.web.dto.TransactionResponse; +import com.devpull.transactionservice.application.dto.command.CreateTransactionCommand; +import com.devpull.transactionservice.application.dto.result.TransactionResult; + +public class TransactionWebMapper { + + private TransactionWebMapper() {} + + /** + * Web → Application + */ + public static CreateTransactionCommand toCommand(TransactionRequest request) { + return new CreateTransactionCommand( + request.accountExternalIdDebit(), + request.accountExternalIdCredit(), + request.transferTypeId(), + request.value() + ); + } + + /** + * Application → Web + */ + public static TransactionResponse toResponse(TransactionResult result) { + return new TransactionResponse( + result.id(), + result.status(), + result.type(), + result.amount(), + result.createdAt(), + result.updatedAt(), + result.accountId() + ); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/AntiFraudWebClientAdapter.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/AntiFraudWebClientAdapter.java new file mode 100644 index 0000000000..b61ab01825 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/AntiFraudWebClientAdapter.java @@ -0,0 +1,26 @@ +package com.devpull.transactionservice.adapter.out.client; + +import com.devpull.transactionservice.application.port.out.AntiFraudPort; +import com.devpull.transactionservice.domain.model.Transaction; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@Component +public class AntiFraudWebClientAdapter implements AntiFraudPort { + + private final WebClient webClient; + + public AntiFraudWebClientAdapter(WebClient webClient) { + this.webClient = webClient; + } + + @Override + public Mono send(Transaction tx) { + return webClient.post() + .uri("http://localhost:8081/api/antifraud") + .bodyValue(tx) + .retrieve() + .bodyToMono(String.class); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/FraudScoringWebClient.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/FraudScoringWebClient.java new file mode 100644 index 0000000000..33314ce9d5 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/FraudScoringWebClient.java @@ -0,0 +1,4 @@ +package com.devpull.transactionservice.adapter.out.client; + +public class FraudScoringWebClient { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/adapter/FraudScoringAdapter.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/adapter/FraudScoringAdapter.java new file mode 100644 index 0000000000..cdf54707c9 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/adapter/FraudScoringAdapter.java @@ -0,0 +1,4 @@ +package com.devpull.transactionservice.adapter.out.client.adapter; + +public class FraudScoringAdapter { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/FraudScoreRequest.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/FraudScoreRequest.java new file mode 100644 index 0000000000..cdcfdaeaf6 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/FraudScoreRequest.java @@ -0,0 +1,4 @@ +package com.devpull.transactionservice.adapter.out.client.dto; + +public class FraudScoreRequest { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/FraudScoreResponse.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/FraudScoreResponse.java new file mode 100644 index 0000000000..e20ba85142 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/FraudScoreResponse.java @@ -0,0 +1,4 @@ +package com.devpull.transactionservice.adapter.out.client.dto; + +public class FraudScoreResponse { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/AccountRepositoryAdapter.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/AccountRepositoryAdapter.java new file mode 100644 index 0000000000..b47a4a43f9 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/AccountRepositoryAdapter.java @@ -0,0 +1,34 @@ +package com.devpull.transactionservice.adapter.out.persistence.adapter; + +import com.devpull.transactionservice.adapter.out.persistence.mapper.AccountPersistenceMapper; +import com.devpull.transactionservice.adapter.out.persistence.repository.R2dbcAccountRepository; +import com.devpull.transactionservice.application.port.out.AccountRepositoryPort; +import com.devpull.transactionservice.domain.model.Account; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +@Component +public class AccountRepositoryAdapter implements AccountRepositoryPort { + + private final R2dbcAccountRepository repository; + + public AccountRepositoryAdapter(R2dbcAccountRepository repository) { + this.repository = repository; + } + + @Override + public Mono save(Account account) { + return repository + .save(AccountPersistenceMapper.toEntity(account)) + .map(AccountPersistenceMapper::toDomain); + } + + @Override + public Mono findById(UUID id) { + return repository + .findById(id) + .map(AccountPersistenceMapper::toDomain); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/TransactionRepositoryAdapter.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/TransactionRepositoryAdapter.java new file mode 100644 index 0000000000..09872e8735 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/TransactionRepositoryAdapter.java @@ -0,0 +1,32 @@ +package com.devpull.transactionservice.adapter.out.persistence.adapter; + +import com.devpull.transactionservice.adapter.out.persistence.mapper.TransactionPersistenceMapper; +import com.devpull.transactionservice.adapter.out.persistence.repository.R2dbcTransactionRepository; +import com.devpull.transactionservice.application.port.out.TransactionRepositoryPort; +import com.devpull.transactionservice.domain.model.Transaction; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +@Component +public class TransactionRepositoryAdapter implements TransactionRepositoryPort { + + private final R2dbcTransactionRepository repository; + + public TransactionRepositoryAdapter(R2dbcTransactionRepository repository) { + this.repository = repository; + } + + @Override + public Mono save(Transaction tx) { + return repository.save(TransactionPersistenceMapper.toEntity(tx)) + .map(TransactionPersistenceMapper::toDomain); + } + + @Override + public Mono findById(UUID id) { + return repository.findById(id) + .map(TransactionPersistenceMapper::toDomain); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/AccountEntity.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/AccountEntity.java new file mode 100644 index 0000000000..f68525c486 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/AccountEntity.java @@ -0,0 +1,36 @@ +package com.devpull.transactionservice.adapter.out.persistence.entity; + +import com.devpull.transactionservice.domain.enums.AccountStatus; +import com.devpull.transactionservice.domain.enums.AccountType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.Instant; +import java.util.UUID; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Table(name = "accounts") +public class AccountEntity { + + @Id + private UUID id; + + @Column("status") + private AccountStatus status; + + @Column("type") + private AccountType type; + + @Column("created_at") + private Instant createdAt; + + @Column("updated_at") + private Instant updatedAt; + +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/TransactionEntity.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/TransactionEntity.java new file mode 100644 index 0000000000..64539ac724 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/TransactionEntity.java @@ -0,0 +1,40 @@ +package com.devpull.transactionservice.adapter.out.persistence.entity; + +import com.devpull.transactionservice.domain.enums.TransactionStatus; +import com.devpull.transactionservice.domain.enums.TransactionType; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.Instant; +import java.util.UUID; + +@NoArgsConstructor +@AllArgsConstructor +@Getter +@Table(name = "transactions") +public class TransactionEntity { + @Id + private UUID id; + + @Column("status") + private TransactionStatus status; + + @Column("type") + private TransactionType type; + + @Column("amount") + private double amount; + + @Column("created_at") + private Instant createdAt; + + @Column("updated_at") + private Instant updatedAt; + + @Column("account_id") + private UUID accountId; +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/AccountPersistenceMapper.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/AccountPersistenceMapper.java new file mode 100644 index 0000000000..f673ccb2ae --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/AccountPersistenceMapper.java @@ -0,0 +1,30 @@ +package com.devpull.transactionservice.adapter.out.persistence.mapper; + +import com.devpull.transactionservice.adapter.out.persistence.entity.AccountEntity; +import com.devpull.transactionservice.domain.model.Account; + +import java.time.Instant; + +public final class AccountPersistenceMapper { + + private AccountPersistenceMapper() {} + + public static AccountEntity toEntity(Account domain) { + return new AccountEntity( + domain.getId(), + domain.getAccountStatus(), + domain.getAccountType(), + domain.getCreatedAt(), + Instant.now() // updated_at + ); + } + + public static Account toDomain(AccountEntity entity) { + return Account.rehydrate( + entity.getId(), + entity.getStatus(), + entity.getType(), + entity.getCreatedAt() + ); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/TransactionPersistenceMapper.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/TransactionPersistenceMapper.java new file mode 100644 index 0000000000..0c8359e1ba --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/TransactionPersistenceMapper.java @@ -0,0 +1,33 @@ +package com.devpull.transactionservice.adapter.out.persistence.mapper; + +import com.devpull.transactionservice.adapter.out.persistence.entity.TransactionEntity; +import com.devpull.transactionservice.domain.model.Transaction; + +public final class TransactionPersistenceMapper { + + private TransactionPersistenceMapper() {} + + public static TransactionEntity toEntity(Transaction domain) { + return new TransactionEntity( + domain.getId(), + domain.getStatus(), + domain.getType(), + domain.getAmount(), + domain.getCreatedAt(), + domain.getUpdatedAt(), + domain.getAccountId() + ); + } + + public static Transaction toDomain(TransactionEntity entity) { + return Transaction.rehydrate( + entity.getId(), + entity.getStatus(), + entity.getType(), + entity.getAmount(), + entity.getCreatedAt(), + entity.getUpdatedAt(), + entity.getAccountId() + ); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/repository/R2dbcAccountRepository.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/repository/R2dbcAccountRepository.java new file mode 100644 index 0000000000..979969a940 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/repository/R2dbcAccountRepository.java @@ -0,0 +1,9 @@ +package com.devpull.transactionservice.adapter.out.persistence.repository; + +import com.devpull.transactionservice.adapter.out.persistence.entity.AccountEntity; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +import java.util.UUID; + +public interface R2dbcAccountRepository extends ReactiveCrudRepository { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/repository/R2dbcTransactionRepository.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/repository/R2dbcTransactionRepository.java new file mode 100644 index 0000000000..7678e0f2da --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/repository/R2dbcTransactionRepository.java @@ -0,0 +1,9 @@ +package com.devpull.transactionservice.adapter.out.persistence.repository; + +import com.devpull.transactionservice.adapter.out.persistence.entity.TransactionEntity; +import org.springframework.data.repository.reactive.ReactiveCrudRepository; + +import java.util.UUID; + +public interface R2dbcTransactionRepository extends ReactiveCrudRepository { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/command/CreateAccountCommand.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/command/CreateAccountCommand.java new file mode 100644 index 0000000000..43c6eb7ad7 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/command/CreateAccountCommand.java @@ -0,0 +1,9 @@ +package com.devpull.transactionservice.application.dto.command; + +import com.devpull.transactionservice.domain.enums.AccountStatus; +import com.devpull.transactionservice.domain.enums.AccountType; + +public record CreateAccountCommand( + AccountStatus accountStatus, + AccountType accountType +) {} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/command/CreateTransactionCommand.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/command/CreateTransactionCommand.java new file mode 100644 index 0000000000..9a4c93322f --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/command/CreateTransactionCommand.java @@ -0,0 +1,10 @@ +package com.devpull.transactionservice.application.dto.command; + +import java.util.UUID; + +public record CreateTransactionCommand( + UUID accountExternalIdDebit, + UUID accountExternalIdCredit, + String transferTypeId, + Double value +) {} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/result/AccountResult.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/result/AccountResult.java new file mode 100644 index 0000000000..82a3bbedde --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/result/AccountResult.java @@ -0,0 +1,12 @@ +package com.devpull.transactionservice.application.dto.result; + +import com.devpull.transactionservice.domain.enums.AccountStatus; +import com.devpull.transactionservice.domain.enums.AccountType; + +import java.util.UUID; + +public record AccountResult( + UUID id, + AccountStatus accountStatus, + AccountType accountType +) {} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/result/TransactionResult.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/result/TransactionResult.java new file mode 100644 index 0000000000..a28440fce1 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/dto/result/TransactionResult.java @@ -0,0 +1,18 @@ +package com.devpull.transactionservice.application.dto.result; + +import com.devpull.transactionservice.domain.enums.TransactionStatus; +import com.devpull.transactionservice.domain.enums.TransactionType; + +import java.time.Instant; +import java.util.UUID; + +public record TransactionResult( + UUID id, + TransactionStatus status, + TransactionType type, + double amount, + Instant createdAt, + Instant updatedAt, + UUID accountId +) +{} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/mapper/AccountApplicationMapper.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/mapper/AccountApplicationMapper.java new file mode 100644 index 0000000000..4ef5dec062 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/mapper/AccountApplicationMapper.java @@ -0,0 +1,18 @@ +package com.devpull.transactionservice.application.mapper; + +import com.devpull.transactionservice.application.dto.result.AccountResult; +import com.devpull.transactionservice.domain.model.Account; + +public final class AccountApplicationMapper { + + private AccountApplicationMapper() {} + + public static AccountResult toResult(Account account) { + return new AccountResult( + account.getId(), + account.getAccountStatus(), + account.getAccountType() + ); + } + +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/CreateAccountUseCase.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/CreateAccountUseCase.java new file mode 100644 index 0000000000..a830ea441d --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/CreateAccountUseCase.java @@ -0,0 +1,9 @@ +package com.devpull.transactionservice.application.port.in; + +import com.devpull.transactionservice.application.dto.command.CreateAccountCommand; +import com.devpull.transactionservice.application.dto.result.AccountResult; +import reactor.core.publisher.Mono; + +public interface CreateAccountUseCase { + Mono createAccount(CreateAccountCommand command); +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/CreateTransactionUseCase.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/CreateTransactionUseCase.java new file mode 100644 index 0000000000..f7cd302ccb --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/CreateTransactionUseCase.java @@ -0,0 +1,11 @@ +package com.devpull.transactionservice.application.port.in; + +import com.devpull.transactionservice.application.dto.command.CreateTransactionCommand; +import com.devpull.transactionservice.application.dto.result.TransactionResult; +import reactor.core.publisher.Mono; + +public interface CreateTransactionUseCase { + + Mono create(CreateTransactionCommand command); + +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/GetAccountUseCase.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/GetAccountUseCase.java new file mode 100644 index 0000000000..6afac2b601 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/GetAccountUseCase.java @@ -0,0 +1,4 @@ +package com.devpull.transactionservice.application.port.in; + +public interface GetAccountUseCase { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/GetTransactionUseCase.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/GetTransactionUseCase.java new file mode 100644 index 0000000000..82828d2dec --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/GetTransactionUseCase.java @@ -0,0 +1,10 @@ +package com.devpull.transactionservice.application.port.in; + +import com.devpull.transactionservice.application.dto.result.TransactionResult; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +public interface GetTransactionUseCase { + Mono getById(UUID transactionId); +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/ListTransactionsUseCase.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/ListTransactionsUseCase.java new file mode 100644 index 0000000000..5ac918c21c --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/ListTransactionsUseCase.java @@ -0,0 +1,4 @@ +package com.devpull.transactionservice.application.port.in; + +public interface ListTransactionsUseCase { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/UpdateTransactionStatusUseCase.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/UpdateTransactionStatusUseCase.java new file mode 100644 index 0000000000..f0607e8c2f --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/in/UpdateTransactionStatusUseCase.java @@ -0,0 +1,9 @@ +package com.devpull.transactionservice.application.port.in; + +import reactor.core.publisher.Mono; + +import java.util.UUID; + +public interface UpdateTransactionStatusUseCase { + Mono updateStatus(UUID transactionId, String status); +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/AccountRepositoryPort.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/AccountRepositoryPort.java new file mode 100644 index 0000000000..f30695c4bb --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/AccountRepositoryPort.java @@ -0,0 +1,14 @@ +package com.devpull.transactionservice.application.port.out; + +import com.devpull.transactionservice.domain.model.Account; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +public interface AccountRepositoryPort { + + Mono save(Account account); + + Mono findById(UUID id); + +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/AntiFraudPort.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/AntiFraudPort.java new file mode 100644 index 0000000000..780d599f93 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/AntiFraudPort.java @@ -0,0 +1,10 @@ +package com.devpull.transactionservice.application.port.out; + +import com.devpull.transactionservice.domain.model.Transaction; +import reactor.core.publisher.Mono; + +public interface AntiFraudPort { + + Mono send(Transaction tx); + +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/TransactionRepositoryPort.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/TransactionRepositoryPort.java new file mode 100644 index 0000000000..031dcf6608 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/TransactionRepositoryPort.java @@ -0,0 +1,14 @@ +package com.devpull.transactionservice.application.port.out; + +import com.devpull.transactionservice.domain.model.Transaction; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +public interface TransactionRepositoryPort { + + Mono save(Transaction tx); + + Mono findById(UUID id); + +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateAccountService.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateAccountService.java new file mode 100644 index 0000000000..d78a3ae782 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateAccountService.java @@ -0,0 +1,42 @@ +package com.devpull.transactionservice.application.usecase; + +import com.devpull.transactionservice.application.dto.command.CreateAccountCommand; +import com.devpull.transactionservice.application.dto.result.AccountResult; +import com.devpull.transactionservice.application.mapper.AccountApplicationMapper; +import com.devpull.transactionservice.application.port.in.CreateAccountUseCase; +import com.devpull.transactionservice.application.port.out.AccountRepositoryPort; +import com.devpull.transactionservice.domain.exception.BadRequestException; +import com.devpull.transactionservice.domain.model.Account; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +@Service +public class CreateAccountService implements CreateAccountUseCase { + + private final AccountRepositoryPort accountRepositoryPort; + + public CreateAccountService(AccountRepositoryPort accountRepositoryPort) { + this.accountRepositoryPort = accountRepositoryPort; + } + + @Override + public Mono createAccount(CreateAccountCommand command) { + + if (command == null) { + return Mono.error(new BadRequestException("Request body is required")); + } + if (command.accountStatus() == null) { + return Mono.error(new BadRequestException("accountStatus is required")); + } + if (command.accountType() == null) { + return Mono.error(new BadRequestException("accountType is required")); + } + + // Domain object creation + Account account = Account.createNew(command.accountStatus(), command.accountType()); + + // Persistence and mapping to result + return accountRepositoryPort.save(account) + .map(AccountApplicationMapper::toResult); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateTransactionService.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateTransactionService.java new file mode 100644 index 0000000000..7f67c812c7 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateTransactionService.java @@ -0,0 +1,85 @@ +package com.devpull.transactionservice.application.usecase; + +import com.devpull.transactionservice.application.dto.command.CreateTransactionCommand; +import com.devpull.transactionservice.application.dto.result.TransactionResult; +import com.devpull.transactionservice.application.port.in.CreateTransactionUseCase; +import com.devpull.transactionservice.application.port.out.AccountRepositoryPort; +import com.devpull.transactionservice.application.port.out.AntiFraudPort; +import com.devpull.transactionservice.application.port.out.TransactionRepositoryPort; +import com.devpull.transactionservice.domain.enums.TransactionType; +import com.devpull.transactionservice.domain.exception.BadRequestException; +import com.devpull.transactionservice.domain.exception.ResourceNotFoundException; +import com.devpull.transactionservice.domain.model.Transaction; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +@Service +public class CreateTransactionService implements CreateTransactionUseCase { + + private final TransactionRepositoryPort txPort; + private final AccountRepositoryPort accountPort; + private final AntiFraudPort antiFraudPort; + + public CreateTransactionService(TransactionRepositoryPort txPort, + AccountRepositoryPort accountPort, + AntiFraudPort antiFraudPort) { + this.txPort = txPort; + this.accountPort = accountPort; + this.antiFraudPort = antiFraudPort; + } + + + @Override + public Mono create(CreateTransactionCommand command) { + if (command == null) return Mono.error(new BadRequestException("Request body is required")); + if (command.value() == null || command.value() <= 0) return Mono.error(new BadRequestException("value must be > 0")); + if (command.transferTypeId() == null || command.transferTypeId().isBlank()) + return Mono.error(new BadRequestException("transferTypeId is required")); + + UUID accountId = (command.accountExternalIdCredit() != null) + ? command.accountExternalIdCredit() + : command.accountExternalIdDebit(); + + if (accountId == null) { + return Mono.error(new BadRequestException("Either accountExternalIdCredit or accountExternalIdDebit is required")); + } + + TransactionType type = mapTransferType(command.transferTypeId()); + + // 1) validate account exists + return accountPort.findById(accountId) + .switchIfEmpty(Mono.error(new ResourceNotFoundException( + "Account", "id", accountId + ))) + // 2) create & save transaction + .then(Mono.defer(() -> txPort.save(Transaction.createNew(accountId, type, command.value())))) + // 3) send to anti-fraud + .flatMap(saved -> + antiFraudPort.send(saved) + .onErrorResume(e -> Mono.just("antifraud_error")) //not fail the whole flow if antifraud fails + .thenReturn(saved) + ) + // 4) map to result object + .map(saved -> new TransactionResult( + saved.getId(), + saved.getStatus(), + saved.getType(), + saved.getAmount(), + saved.getCreatedAt(), + saved.getUpdatedAt(), + saved.getAccountId() + )); + + } + + private TransactionType mapTransferType(String transferTypeId) { + try { + return TransactionType.valueOf(transferTypeId.trim().toUpperCase()); + } catch (Exception e) { + // fallback (o BadRequest) + return TransactionType.FAST; + } + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/GetAccountService.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/GetAccountService.java new file mode 100644 index 0000000000..8c4addc679 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/GetAccountService.java @@ -0,0 +1,4 @@ +package com.devpull.transactionservice.application.usecase; + +public class GetAccountService { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/GetTransactionService.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/GetTransactionService.java new file mode 100644 index 0000000000..8d962f3295 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/GetTransactionService.java @@ -0,0 +1,35 @@ +package com.devpull.transactionservice.application.usecase; + +import com.devpull.transactionservice.application.dto.result.TransactionResult; +import com.devpull.transactionservice.application.port.in.GetTransactionUseCase; +import com.devpull.transactionservice.application.port.out.TransactionRepositoryPort; +import com.devpull.transactionservice.domain.exception.ResourceNotFoundException; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +@Service +public class GetTransactionService implements GetTransactionUseCase { + + private final TransactionRepositoryPort txPort; + + public GetTransactionService(TransactionRepositoryPort txPort) { + this.txPort = txPort; + } + + @Override + public Mono getById(UUID transactionId) { + return txPort.findById(transactionId) + .switchIfEmpty(Mono.error(new ResourceNotFoundException("Transaction", "id", transactionId))) + .map(tx -> new TransactionResult( + tx.getId(), + tx.getStatus(), + tx.getType(), + tx.getAmount(), + tx.getCreatedAt(), + tx.getUpdatedAt(), + tx.getAccountId() + )); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/ListTransactionsService.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/ListTransactionsService.java new file mode 100644 index 0000000000..ed50b01fb1 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/ListTransactionsService.java @@ -0,0 +1,4 @@ +package com.devpull.transactionservice.application.usecase; + +public class ListTransactionsService { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/UpdateTransactionStatusService.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/UpdateTransactionStatusService.java new file mode 100644 index 0000000000..c914992cbf --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/UpdateTransactionStatusService.java @@ -0,0 +1,33 @@ +package com.devpull.transactionservice.application.usecase; + +import com.devpull.transactionservice.application.port.in.UpdateTransactionStatusUseCase; +import com.devpull.transactionservice.application.port.out.TransactionRepositoryPort; +import com.devpull.transactionservice.domain.enums.TransactionStatus; +import com.devpull.transactionservice.domain.exception.ResourceNotFoundException; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.util.UUID; + +@Service +public class UpdateTransactionStatusService implements UpdateTransactionStatusUseCase { + + private final TransactionRepositoryPort txPort; + + public UpdateTransactionStatusService(TransactionRepositoryPort txPort) { + this.txPort = txPort; + } + + @Override + public Mono updateStatus(UUID transactionId, String status) { + TransactionStatus newStatus = TransactionStatus.valueOf(status.trim().toUpperCase()); + + return txPort.findById(transactionId) + .switchIfEmpty(Mono.error(new ResourceNotFoundException("Transaction", "id", transactionId))) + .flatMap(tx -> { + tx.setStatus(newStatus); + return txPort.save(tx); + }) + .then(); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/AccountStatus.java b/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/AccountStatus.java new file mode 100644 index 0000000000..1d46a2803d --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/AccountStatus.java @@ -0,0 +1,6 @@ +package com.devpull.transactionservice.domain.enums; + +public enum AccountStatus { + AVAILABLE, + UNAVAILABLE, +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/AccountType.java b/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/AccountType.java new file mode 100644 index 0000000000..3c5f94c0c3 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/AccountType.java @@ -0,0 +1,6 @@ +package com.devpull.transactionservice.domain.enums; + +public enum AccountType { + CREDIT, + DEBIT +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionStatus.java b/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionStatus.java new file mode 100644 index 0000000000..89240e904e --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionStatus.java @@ -0,0 +1,7 @@ +package com.devpull.transactionservice.domain.enums; + +public enum TransactionStatus { + PENDING, + APPROVED, + REJECT +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionType.java b/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionType.java new file mode 100644 index 0000000000..aaa1c4fb03 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionType.java @@ -0,0 +1,8 @@ +package com.devpull.transactionservice.domain.enums; + +public enum TransactionType { + EXPRESS, + FAST, + COMMON, + UNUSUAL +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/APIException.java b/transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/APIException.java new file mode 100644 index 0000000000..ca1d62388c --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/APIException.java @@ -0,0 +1,16 @@ +package com.devpull.transactionservice.domain.exception; + +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public class APIException extends RuntimeException{ + + private final HttpStatus httpStatus; + + public APIException(HttpStatus httpStatus, String message) { + super(message); + this.httpStatus = httpStatus; + } + +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/BadRequestException.java b/transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/BadRequestException.java new file mode 100644 index 0000000000..f5cf87d8fd --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/BadRequestException.java @@ -0,0 +1,8 @@ +package com.devpull.transactionservice.domain.exception; + +public class BadRequestException extends RuntimeException { + + public BadRequestException(String message) { + super(message); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/ResourceNotFoundException.java b/transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/ResourceNotFoundException.java new file mode 100644 index 0000000000..6d19406e4e --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/domain/exception/ResourceNotFoundException.java @@ -0,0 +1,21 @@ +package com.devpull.transactionservice.domain.exception; + +import lombok.Getter; + +import java.util.UUID; + +@Getter +public class ResourceNotFoundException extends RuntimeException { + + private final String resourceName; + private final String fieldName; + private final UUID fieldValue; + + public ResourceNotFoundException(String resourceName, String fieldName, UUID fieldValue) { + super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue)); + this.resourceName = resourceName; + this.fieldName = fieldName; + this.fieldValue = fieldValue; + } + +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/domain/model/Account.java b/transaction-service/src/main/java/com/devpull/transactionservice/domain/model/Account.java new file mode 100644 index 0000000000..b1384ad067 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/domain/model/Account.java @@ -0,0 +1,80 @@ +package com.devpull.transactionservice.domain.model; + +import com.devpull.transactionservice.domain.enums.AccountStatus; +import com.devpull.transactionservice.domain.enums.AccountType; +import lombok.Getter; + +import java.time.Instant; +import java.util.Objects; +import java.util.UUID; + +@Getter +public class Account { + + private final UUID id; + private AccountStatus accountStatus; + private final AccountType accountType; + private final Instant createdAt; + + private Account(UUID id, AccountStatus accountStatus, AccountType accountType, Instant createdAt) { + this.id = Objects.requireNonNull(id, "id must not be null"); + this.accountStatus = Objects.requireNonNull(accountStatus, "accountStatus must not be null"); + this.accountType = Objects.requireNonNull(accountType, "accountType must not be null"); + this.createdAt = Objects.requireNonNull(createdAt, "createdAt must not be null"); + + validateInvariants(); + } + + /** + * Factory: to create a new account obj. + */ + public static Account createNew(AccountStatus accountStatus, AccountType accountType) { + return new Account( + UUID.randomUUID(), + accountStatus, + accountType, + Instant.now() + ); + } + + /** + * Factory: to work from persistence slice (already exists). + */ + public static Account rehydrate(UUID id, AccountStatus accountStatus, AccountType accountType, Instant createdAt) { + return new Account(id, accountStatus, accountType, createdAt); + } + + /** + * Domain behavior example. + */ + public void activate() { + if (this.accountStatus == AccountStatus.AVAILABLE) return; + + if (this.accountStatus == AccountStatus.UNAVAILABLE) { + throw new IllegalStateException("A closed account cannot be activated"); + } + this.accountStatus = AccountStatus.AVAILABLE; + } + + public void suspend() { + if (this.accountStatus == AccountStatus.UNAVAILABLE) { + throw new IllegalStateException("A closed account cannot be suspended"); + } + this.accountStatus = AccountStatus.UNAVAILABLE; + } + + public void close() { + this.accountStatus = AccountStatus.UNAVAILABLE; + } + + public boolean isActive() { + return this.accountStatus == AccountStatus.AVAILABLE; + } + + private void validateInvariants() { + if (accountStatus == AccountStatus.UNAVAILABLE) { + throw new IllegalArgumentException("Account cannot be created with status CLOSED"); + } + } + +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/domain/model/Transaction.java b/transaction-service/src/main/java/com/devpull/transactionservice/domain/model/Transaction.java new file mode 100644 index 0000000000..c084a1f270 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/domain/model/Transaction.java @@ -0,0 +1,48 @@ +package com.devpull.transactionservice.domain.model; + +import com.devpull.transactionservice.domain.enums.TransactionStatus; +import com.devpull.transactionservice.domain.enums.TransactionType; +import lombok.Getter; + +import java.time.Instant; +import java.util.Objects; +import java.util.UUID; + +@Getter +public class Transaction { + + private final UUID id; + private TransactionStatus status; + private final TransactionType type; + private final double amount; + private final Instant createdAt; + private final Instant updatedAt; + private final UUID accountId; + + private Transaction(UUID id, TransactionStatus status, TransactionType type, double amount, + Instant createdAt, Instant updatedAt, UUID accountId) { + this.id = Objects.requireNonNull(id); + this.status = Objects.requireNonNull(status); + this.type = Objects.requireNonNull(type); + if (amount <= 0) throw new IllegalArgumentException("amount must be > 0"); + this.amount = amount; + this.createdAt = Objects.requireNonNull(createdAt); + this.updatedAt = Objects.requireNonNull(updatedAt); + this.accountId = Objects.requireNonNull(accountId); + } + + public static Transaction createNew(UUID accountId, TransactionType type, double amount) { + Instant now = Instant.now(); + return new Transaction(UUID.randomUUID(), TransactionStatus.PENDING, type, amount, now, now, accountId); + } + + public static Transaction rehydrate(UUID id, TransactionStatus status, TransactionType type, double amount, + Instant createdAt, Instant updatedAt, UUID accountId) { + return new Transaction(id, status, type, amount, createdAt, updatedAt, accountId); + } + + public void setStatus(TransactionStatus status) { + this.status = Objects.requireNonNull(status); + } + +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/domain/service/FraudPolicy.java b/transaction-service/src/main/java/com/devpull/transactionservice/domain/service/FraudPolicy.java new file mode 100644 index 0000000000..ebeb71d13a --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/domain/service/FraudPolicy.java @@ -0,0 +1,4 @@ +package com.devpull.transactionservice.domain.service; + +public class FraudPolicy { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/OpenApiConfig.java b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/OpenApiConfig.java new file mode 100644 index 0000000000..3787465672 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/OpenApiConfig.java @@ -0,0 +1,4 @@ +package com.devpull.transactionservice.infrastructure.config; + +public class OpenApiConfig { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/PersistenceConfig.java b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/PersistenceConfig.java new file mode 100644 index 0000000000..63eda02148 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/PersistenceConfig.java @@ -0,0 +1,114 @@ +package com.devpull.transactionservice.infrastructure.config; + +import com.devpull.transactionservice.domain.enums.AccountStatus; +import com.devpull.transactionservice.domain.enums.AccountType; +import com.devpull.transactionservice.domain.enums.TransactionStatus; +import com.devpull.transactionservice.domain.enums.TransactionType; +import io.r2dbc.spi.ConnectionFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.r2dbc.convert.R2dbcCustomConversions; +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.r2dbc.connection.R2dbcTransactionManager; +import org.springframework.transaction.ReactiveTransactionManager; +import org.springframework.transaction.reactive.TransactionalOperator; + +import java.util.List; + +@Configuration +public class PersistenceConfig { + + @Bean + public R2dbcEntityTemplate r2dbcEntityTemplate(ConnectionFactory connectionFactory) { + return new R2dbcEntityTemplate(connectionFactory); + } + + @Bean + public ReactiveTransactionManager reactiveTransactionManager(ConnectionFactory connectionFactory) { + return new R2dbcTransactionManager(connectionFactory); + } + + @Bean + public TransactionalOperator transactionalOperator(ReactiveTransactionManager txManager) { + return TransactionalOperator.create(txManager); + } + + @Bean + public R2dbcCustomConversions r2dbcCustomConversions() { + List> converters = List.of( + new TransactionStatusReadConverter(), + new TransactionStatusWriteConverter(), + new TransactionTypeReadConverter(), + new TransactionTypeWriteConverter(), + new AccountStatusReadConverter(), + new AccountStatusWriteConverter(), + new AccountTypeReadConverter(), + new AccountTypeWriteConverter() + ); + + return new R2dbcCustomConversions(R2dbcCustomConversions.STORE_CONVERSIONS, converters); + } + + // ---------- TransactionStatus ---------- + @ReadingConverter + static class TransactionStatusReadConverter implements Converter { + @Override public TransactionStatus convert(String source) { + return source == null ? null : TransactionStatus.valueOf(source.trim().toUpperCase()); + } + } + + @WritingConverter + static class TransactionStatusWriteConverter implements Converter { + @Override public String convert(TransactionStatus source) { + return source == null ? null : source.name(); + } + } + + // ---------- TransactionType ---------- + @ReadingConverter + static class TransactionTypeReadConverter implements Converter { + @Override public TransactionType convert(String source) { + return source == null ? null : TransactionType.valueOf(source.trim().toUpperCase()); + } + } + + @WritingConverter + static class TransactionTypeWriteConverter implements Converter { + @Override public String convert(TransactionType source) { + return source == null ? null : source.name(); + } + } + + // ---------- AccountStatus ---------- + @ReadingConverter + static class AccountStatusReadConverter implements Converter { + @Override public AccountStatus convert(String source) { + return source == null ? null : AccountStatus.valueOf(source.trim().toUpperCase()); + } + } + + @WritingConverter + static class AccountStatusWriteConverter implements Converter { + @Override public String convert(AccountStatus source) { + return source == null ? null : source.name(); + } + } + + // ---------- AccountType ---------- + @ReadingConverter + static class AccountTypeReadConverter implements Converter { + @Override public AccountType convert(String source) { + return source == null ? null : AccountType.valueOf(source.trim().toUpperCase()); + } + } + + @WritingConverter + static class AccountTypeWriteConverter implements Converter { + @Override public String convert(AccountType source) { + return source == null ? null : source.name(); + } + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/WebClientConfig.java b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/WebClientConfig.java new file mode 100644 index 0000000000..93428ef23e --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/config/WebClientConfig.java @@ -0,0 +1,26 @@ +package com.devpull.transactionservice.infrastructure.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.netty.http.client.HttpClient; + + +@Configuration +public class WebClientConfig { + + private static final int TIMEOUT = 5000; + + @Bean + public WebClient webClient() { + HttpClient httpClient = HttpClient.create() + .responseTimeout(java.time.Duration.ofMillis(TIMEOUT)); + + ClientHttpConnector connector = new ReactorClientHttpConnector(httpClient); + return WebClient.builder() + .clientConnector(connector) + .build(); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/ApiExceptionMapper.java b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/ApiExceptionMapper.java new file mode 100644 index 0000000000..e5ed787049 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/ApiExceptionMapper.java @@ -0,0 +1,83 @@ +package com.devpull.transactionservice.infrastructure.exception; + +import com.devpull.transactionservice.domain.exception.APIException; +import com.devpull.transactionservice.domain.exception.BadRequestException; +import com.devpull.transactionservice.domain.exception.ResourceNotFoundException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.ServerWebInputException; + +import java.util.Map; + +@Component +public class ApiExceptionMapper { + + public ResponseEntity toResponse(Throwable ex, ServerWebExchange exchange) { + String path = exchange.getRequest().getPath().value(); + + // Not found + if (ex instanceof ResourceNotFoundException) { + return build(HttpStatus.NOT_FOUND, ex.getMessage(), path, null); + } + + // Bad request + if (ex instanceof BadRequestException) { + return build(HttpStatus.BAD_REQUEST, ex.getMessage(), path, null); + } + + // APIException + if (ex instanceof APIException apiEx) { + HttpStatus status = apiEx.getHttpStatus() != null ? apiEx.getHttpStatus() : HttpStatus.CONFLICT; + return build(status, apiEx.getMessage(), path, null); + } + + // Validation / body malformed / query param not valid (WebFlux) + // sometimes ServerWebInputException wrap binding error, decoding, etc. + if (ex instanceof ServerWebInputException) { + return build(HttpStatus.BAD_REQUEST, safeMessage(ex), path, null); + } + + // If in some point launch ResponseStatusException (WebFlux normal behavior) + if (ex instanceof ResponseStatusException rse) { + HttpStatus status = HttpStatus.resolve(rse.getStatusCode().value()); + if (status == null) status = HttpStatus.INTERNAL_SERVER_ERROR; + return build(status, safeMessage(rse), path, null); + } + + // Fallback + return build(HttpStatus.INTERNAL_SERVER_ERROR, "Unexpected error", path, null); + } + + public ResponseEntity toValidationResponse( + Map fieldErrors, + ServerWebExchange exchange + ) { + String path = exchange.getRequest().getPath().value(); + return build(HttpStatus.BAD_REQUEST, "Validation failed", path, fieldErrors); + } + + private ResponseEntity build( + HttpStatus status, + String message, + String path, + Map validationErrors + ) { + ErrorDetails body = ErrorDetails.builder() + .status(status.value()) + .error(status.getReasonPhrase()) + .message(message) + .path(path) + .validationErrors(validationErrors == null || validationErrors.isEmpty() ? null : validationErrors) + .build(); + + return ResponseEntity.status(status).body(body); + } + + private String safeMessage(Throwable ex) { + // Avoid nulls surprises in response + return (ex.getMessage() == null || ex.getMessage().isBlank()) ? ex.getClass().getSimpleName() : ex.getMessage(); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/ErrorDetails.java b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/ErrorDetails.java new file mode 100644 index 0000000000..305332c58e --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/ErrorDetails.java @@ -0,0 +1,77 @@ +package com.devpull.transactionservice.infrastructure.exception; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; + +import java.time.Instant; +import java.util.Map; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@Getter +public class ErrorDetails { + + private final Instant timestamp; + private final int status; + private final String error; + private final String message; + private final String path; + private final Map validationErrors; + + private ErrorDetails(Builder builder) { + this.timestamp = builder.timestamp; + this.status = builder.status; + this.error = builder.error; + this.message = builder.message; + this.path = builder.path; + this.validationErrors = builder.validationErrors; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Instant timestamp = Instant.now(); + private int status; + private String error; + private String message; + private String path; + private Map validationErrors; + + public Builder timestamp(Instant timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder status(int status) { + this.status = status; + return this; + } + + public Builder error(String error) { + this.error = error; + return this; + } + + public Builder message(String message) { + this.message = message; + return this; + } + + public Builder path(String path) { + this.path = path; + return this; + } + + public Builder validationErrors(Map validationErrors) { + this.validationErrors = validationErrors; + return this; + } + + public ErrorDetails build() { + return new ErrorDetails(this); + } + } +} + + diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/GlobalExceptionHandler.java b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000000..52b06a5be0 --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/infrastructure/exception/GlobalExceptionHandler.java @@ -0,0 +1,47 @@ +package com.devpull.transactionservice.infrastructure.exception; + +import org.springframework.core.annotation.Order; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.bind.support.WebExchangeBindException; +import org.springframework.web.server.ServerWebExchange; +import reactor.core.publisher.Mono; + +import java.util.LinkedHashMap; +import java.util.Map; + +@RestControllerAdvice +@Order(-2) // to catch it early +public class GlobalExceptionHandler { + + private final ApiExceptionMapper mapper; + + public GlobalExceptionHandler(ApiExceptionMapper mapper) { + this.mapper = mapper; + } + + /** + * Validation @Valid in WebFlux normally throw WebExchangeBindException. + */ + @ExceptionHandler(WebExchangeBindException.class) + public Mono> handleValidation( + WebExchangeBindException ex, + ServerWebExchange exchange + ) { + Map errors = new LinkedHashMap<>(); + for (FieldError fe : ex.getFieldErrors()) { + errors.put(fe.getField(), fe.getDefaultMessage()); + } + return Mono.just(mapper.toValidationResponse(errors, exchange)); + } + + /** + * Handler global: map any know exception or unknown to proper response. + */ + @ExceptionHandler(Throwable.class) + public Mono> handleAny(Throwable ex, ServerWebExchange exchange) { + return Mono.just(mapper.toResponse(ex, exchange)); + } +} diff --git a/transaction-service/src/main/resources/application.yml b/transaction-service/src/main/resources/application.yml new file mode 100644 index 0000000000..76361b6a85 --- /dev/null +++ b/transaction-service/src/main/resources/application.yml @@ -0,0 +1,40 @@ +server: + port: ${SERVER_PORT:8080} + +spring: + application: + name: transaction-service + + main: + web-application-type: reactive + + r2dbc: + url: ${R2DBC_URL:r2dbc:postgresql://localhost:5433/app-java-codechallenge} + username: ${DB_USERNAME:postgres} + password: ${DB_PASSWORD:postgres} + + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} + + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + + consumer: + group-id: ${KAFKA_CONSUMER_GROUP:transaction-service} + auto-offset-reset: earliest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring: + json: + trusted: + packages: "*" + + topic: + name: ${KAFKA_TOPIC_TRANSACTIONS:transactions_fraud} + +logging: + level: + io.r2dbc.postgresql.QUERY: ${LOG_R2DBC_QUERY:INFO} + io.r2dbc.postgresql.PARAM: ${LOG_R2DBC_PARAM:INFO} diff --git a/transaction-service/src/test/java/com/devpull/transactionservice/TransactionServiceApplicationTests.java b/transaction-service/src/test/java/com/devpull/transactionservice/TransactionServiceApplicationTests.java new file mode 100644 index 0000000000..a63d292cfd --- /dev/null +++ b/transaction-service/src/test/java/com/devpull/transactionservice/TransactionServiceApplicationTests.java @@ -0,0 +1,13 @@ +package com.devpull.transactionservice; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class TransactionServiceApplicationTests { + + @Test + void contextLoads() { + } + +} From ee0b1ffe272a9d54ac6001322c965a87b9e74243 Mon Sep 17 00:00:00 2001 From: Luis Gutierrez Date: Sat, 31 Jan 2026 04:59:17 -0500 Subject: [PATCH 2/2] final fix in configurations to work listen kafka consumer --- .idea/workspace.xml | 141 +++++++----------- antifraud-service/pom.xml | 9 +- .../config/KafkaProducerConfig.java | 14 +- .../controller/AntifraudController.java | 31 +++- .../dto/TransactionCreatedEvent.java | 14 ++ .../dto/TransactionStatusChangedEvent.java | 15 ++ .../enums/TransactionStatus.java | 6 + .../antifraudservice/kafka/EventProducer.java | 27 ++-- .../service/AntifraudEvaluationService.java | 31 ++++ .../src/main/resources/application.yml | 14 +- transaction-service/pom.xml | 5 + .../TransactionServiceApplication.java | 4 +- .../in/kafka/AntifraudResultListener.java | 37 +++++ .../adapter/in/kafka/KafkaEnableConfig.java | 9 ++ .../in/kafka/TransactionStatusConsumer.java | 25 ---- .../in/kafka/config/KafkaConsumerConfig.java | 48 ++++++ .../kafka/TransactionStatusChangedEvent.java | 11 ++ .../out/client/AntiFraudWebClientAdapter.java | 8 +- .../client/dto/TransactionCreatedEvent.java | 12 ++ .../client/mapper/AntiFraudEventMapper.java | 18 +++ .../adapter/TransactionRepositoryAdapter.java | 9 +- .../out/persistence/entity/AccountEntity.java | 20 ++- .../persistence/entity/TransactionEntity.java | 25 +++- .../mapper/AccountPersistenceMapper.java | 11 +- .../mapper/TransactionPersistenceMapper.java | 28 +++- .../port/out/TransactionRepositoryPort.java | 2 + .../usecase/CreateAccountService.java | 4 +- .../UpdateTransactionStatusService.java | 6 +- .../domain/enums/TransactionStatus.java | 2 +- .../src/main/resources/application.yml | 27 ++-- 30 files changed, 426 insertions(+), 187 deletions(-) create mode 100644 antifraud-service/src/main/java/com/devpull/antifraudservice/dto/TransactionCreatedEvent.java create mode 100644 antifraud-service/src/main/java/com/devpull/antifraudservice/dto/TransactionStatusChangedEvent.java create mode 100644 antifraud-service/src/main/java/com/devpull/antifraudservice/enums/TransactionStatus.java create mode 100644 antifraud-service/src/main/java/com/devpull/antifraudservice/service/AntifraudEvaluationService.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/AntifraudResultListener.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/KafkaEnableConfig.java delete mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/TransactionStatusConsumer.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/config/KafkaConsumerConfig.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/kafka/TransactionStatusChangedEvent.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/TransactionCreatedEvent.java create mode 100644 transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/mapper/AntiFraudEventMapper.java diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 4144e315f3..2542bcd071 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,69 +5,36 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -85,6 +53,7 @@ @@ -112,26 +81,31 @@ - { + "keyToString": { + "ModuleVcsDetector.initialDetectionPerformed": "true", + "RequestMappingsPanelOrder0": "0", + "RequestMappingsPanelOrder1": "1", + "RequestMappingsPanelWidth0": "75", + "RequestMappingsPanelWidth1": "75", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", + "RunOnceActivity.git.unshallow": "true", + "RunOnceActivity.typescript.service.memoryLimit.init": "true", + "Spring Boot.AntifraudServiceApplication.executor": "Run", + "Spring Boot.TransactionServiceApplication.executor": "Run", + "git-widget-placeholder": "feature/implements-antifraud-flux", + "kotlin-language-version-configured": "true", + "last_opened_file_path": "/Users/luis/IdeaProjects/app-java-codechallenge", + "nodejs_package_manager_path": "npm", + "vue.rearranger.settings.migration": "true" } -}]]> +} + + + + + diff --git a/antifraud-service/pom.xml b/antifraud-service/pom.xml index 98e82d2028..4695534899 100644 --- a/antifraud-service/pom.xml +++ b/antifraud-service/pom.xml @@ -42,11 +42,14 @@ org.springframework.boot spring-boot-starter-webflux + + + + org.springframework.kafka spring-kafka - org.springframework.boot spring-boot-devtools @@ -68,10 +71,6 @@ transaction-service 0.0.1-SNAPSHOT - - org.springframework.kafka - spring-kafka - com.fasterxml.jackson.core jackson-databind diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaProducerConfig.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaProducerConfig.java index d038a174bd..3ef3ba4c85 100644 --- a/antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaProducerConfig.java +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/config/KafkaProducerConfig.java @@ -1,8 +1,9 @@ package com.devpull.antifraudservice.config; -import com.fasterxml.jackson.databind.ser.std.StringSerializer; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.common.serialization.ByteArraySerializer; +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.core.DefaultKafkaProducerFactory; @@ -16,16 +17,19 @@ public class KafkaProducerConfig { @Bean - public ProducerFactory producerFactory() { + public ProducerFactory producerFactory( + @Value("${spring.kafka.bootstrap-servers}") String bootstrapServers + ) { Map props = new HashMap<>(); - props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class); + props.put("spring.json.add.type.headers", false); return new DefaultKafkaProducerFactory<>(props); } @Bean - public KafkaTemplate kafkaTemplate() { - return new KafkaTemplate<>(producerFactory()); + public KafkaTemplate kafkaTemplate(ProducerFactory pf) { + return new KafkaTemplate<>(pf); } } diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/controller/AntifraudController.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/controller/AntifraudController.java index 06959bb97d..fd5e741154 100644 --- a/antifraud-service/src/main/java/com/devpull/antifraudservice/controller/AntifraudController.java +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/controller/AntifraudController.java @@ -1,8 +1,13 @@ package com.devpull.antifraudservice.controller; +import com.devpull.antifraudservice.dto.TransactionCreatedEvent; +import com.devpull.antifraudservice.dto.TransactionStatusChangedEvent; import com.devpull.antifraudservice.kafka.EventProducer; +import com.devpull.antifraudservice.service.AntifraudEvaluationService; import com.devpull.transactionservice.domain.enums.TransactionStatus; -import com.devpull.transactionservice.domain.model.Transaction; +import jakarta.validation.Valid; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -11,20 +16,32 @@ @RestController @RequestMapping("/api/v1/antifraud") +@Slf4j public class AntifraudController { private final EventProducer eventProducer; - public AntifraudController(EventProducer eventProducer){ + private final AntifraudEvaluationService evaluationService; + + + public AntifraudController(EventProducer eventProducer, + AntifraudEvaluationService evaluationService) { this.eventProducer = eventProducer; + this.evaluationService = evaluationService; } @PostMapping - public String evaluateAntifraud(@RequestBody Transaction transaction) throws Exception { - String msj = transaction.getAmount() > 1000 ? "REJECT" : "APPROVED"; - transaction.setStatus(TransactionStatus.valueOf(msj)); - eventProducer.sendMessage(transaction); + public ResponseEntity evaluateAntifraud(@Valid @RequestBody TransactionCreatedEvent transaction) { + log.info("[ANTIFRAUD] Evaluating transactionId={}, amount={}", + transaction.transactionId(), transaction.amount()); + + TransactionStatusChangedEvent resultEvent = evaluationService.evaluate(transaction); + + eventProducer.sendMessage(resultEvent); + + log.info("[ANTIFRAUD] Evaluation done transactionId={}, status={}", + resultEvent.transactionId(), resultEvent.status()); - return "Event handled successfully!"; + return ResponseEntity.ok("Event handled successfully!"); } } diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/dto/TransactionCreatedEvent.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/dto/TransactionCreatedEvent.java new file mode 100644 index 0000000000..ee3a46e058 --- /dev/null +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/dto/TransactionCreatedEvent.java @@ -0,0 +1,14 @@ +package com.devpull.antifraudservice.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; + +import java.util.UUID; + +public record TransactionCreatedEvent( + @NotNull UUID transactionId, + @NotNull UUID accountId, + @NotNull String type, + @NotNull @Positive Double amount +) { +} diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/dto/TransactionStatusChangedEvent.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/dto/TransactionStatusChangedEvent.java new file mode 100644 index 0000000000..b0a07805fa --- /dev/null +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/dto/TransactionStatusChangedEvent.java @@ -0,0 +1,15 @@ +package com.devpull.antifraudservice.dto; + +import com.devpull.antifraudservice.enums.TransactionStatus; +import jakarta.validation.constraints.NotNull; + +import java.time.Instant; +import java.util.UUID; + +public record TransactionStatusChangedEvent( + @NotNull UUID transactionId, + @NotNull TransactionStatus status, + Instant evaluatedAt, + String reason +) { +} diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/enums/TransactionStatus.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/enums/TransactionStatus.java new file mode 100644 index 0000000000..c4a87d8d75 --- /dev/null +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/enums/TransactionStatus.java @@ -0,0 +1,6 @@ +package com.devpull.antifraudservice.enums; + +public enum TransactionStatus { + APPROVED, + REJECTED +} diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/kafka/EventProducer.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/kafka/EventProducer.java index 9bb1221abe..a621413e14 100644 --- a/antifraud-service/src/main/java/com/devpull/antifraudservice/kafka/EventProducer.java +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/kafka/EventProducer.java @@ -1,12 +1,9 @@ package com.devpull.antifraudservice.kafka; -import com.devpull.transactionservice.domain.model.Transaction; +import com.devpull.antifraudservice.dto.TransactionStatusChangedEvent; import lombok.extern.slf4j.Slf4j; import org.apache.kafka.clients.admin.NewTopic; import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.support.KafkaHeaders; -import org.springframework.messaging.Message; -import org.springframework.messaging.support.MessageBuilder; import org.springframework.stereotype.Service; import tools.jackson.databind.ObjectMapper; @@ -16,24 +13,24 @@ public class EventProducer { private final KafkaTemplate kafkaTemplate; private final ObjectMapper objectMapper; - private final NewTopic topic; + private final String topicName; - public EventProducer( - KafkaTemplate kafkaTemplate, - ObjectMapper objectMapper, - NewTopic topic - ) { + public EventProducer(KafkaTemplate kafkaTemplate, + NewTopic topic, + ObjectMapper objectMapper) { this.kafkaTemplate = kafkaTemplate; this.objectMapper = objectMapper; - this.topic = topic; + this.topicName = topic.name(); } - public void sendMessage(Transaction transaction) { + public void sendMessage(TransactionStatusChangedEvent event) { try { - byte[] payload = objectMapper.writeValueAsBytes(transaction); - kafkaTemplate.send(topic.name(), payload); + byte[] payload = objectMapper.writeValueAsBytes(event); + kafkaTemplate.send(topicName, event.transactionId().toString(), payload); + log.info("[ANTIFRAUD] Sent status event txId={}, status={}", event.transactionId(), event.status()); } catch (Exception e) { - log.error("Error serializing transaction event", e); + log.error("[ANTIFRAUD] Failed to serialize/send event", e); + throw new RuntimeException("Failed to produce event", e); } } diff --git a/antifraud-service/src/main/java/com/devpull/antifraudservice/service/AntifraudEvaluationService.java b/antifraud-service/src/main/java/com/devpull/antifraudservice/service/AntifraudEvaluationService.java new file mode 100644 index 0000000000..baac50c93b --- /dev/null +++ b/antifraud-service/src/main/java/com/devpull/antifraudservice/service/AntifraudEvaluationService.java @@ -0,0 +1,31 @@ +package com.devpull.antifraudservice.service; + +import com.devpull.antifraudservice.dto.TransactionCreatedEvent; +import com.devpull.antifraudservice.dto.TransactionStatusChangedEvent; +import com.devpull.antifraudservice.enums.TransactionStatus; +import org.springframework.stereotype.Service; + +import java.time.Instant; + +@Service +public class AntifraudEvaluationService { + + private static final double THRESHOLD = 1000.0; + + public TransactionStatusChangedEvent evaluate(TransactionCreatedEvent tx) { + boolean rejected = tx.amount() != null && tx.amount() > THRESHOLD; + + TransactionStatus status = rejected ? TransactionStatus.REJECTED : TransactionStatus.APPROVED; + + String reason = rejected + ? "Amount exceeds threshold (" + THRESHOLD + ")" + : "OK"; + + return new TransactionStatusChangedEvent( + tx.transactionId(), + status, + Instant.now(), + reason + ); + } +} diff --git a/antifraud-service/src/main/resources/application.yml b/antifraud-service/src/main/resources/application.yml index 3cd584726d..acdb69a797 100644 --- a/antifraud-service/src/main/resources/application.yml +++ b/antifraud-service/src/main/resources/application.yml @@ -15,16 +15,16 @@ spring: group-id: ${KAFKA_CONSUMER_GROUP:antifraud-service} auto-offset-reset: earliest key-deserializer: org.apache.kafka.common.serialization.StringDeserializer - value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer - properties: - spring: - json: - trusted: - packages: "*" + value-deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer - value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + value-serializer: org.apache.kafka.common.serialization.ByteArraySerializer topic: name: ${KAFKA_TOPIC_TRANSACTIONS:transactions_fraud} + +logging: + level: + io.r2dbc.postgresql.QUERY: ${LOG_R2DBC_QUERY:DEBUG} + io.r2dbc.postgresql.PARAM: ${LOG_R2DBC_PARAM:DEBUG} diff --git a/transaction-service/pom.xml b/transaction-service/pom.xml index d3d9708414..0d49c4f5f6 100644 --- a/transaction-service/pom.xml +++ b/transaction-service/pom.xml @@ -96,6 +96,11 @@ 4.1.0-M1 compile + + io.netty + netty-resolver-dns-native-macos + osx-aarch_64 + diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/TransactionServiceApplication.java b/transaction-service/src/main/java/com/devpull/transactionservice/TransactionServiceApplication.java index abe5dc3659..0dc996c18d 100644 --- a/transaction-service/src/main/java/com/devpull/transactionservice/TransactionServiceApplication.java +++ b/transaction-service/src/main/java/com/devpull/transactionservice/TransactionServiceApplication.java @@ -1,13 +1,13 @@ package com.devpull.transactionservice; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class TransactionServiceApplication { - public static void main(String[] args) { SpringApplication.run(TransactionServiceApplication.class, args); } - } diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/AntifraudResultListener.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/AntifraudResultListener.java new file mode 100644 index 0000000000..1cfe2baf5b --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/AntifraudResultListener.java @@ -0,0 +1,37 @@ +package com.devpull.transactionservice.adapter.in.kafka; + +import com.devpull.transactionservice.adapter.in.web.dto.kafka.TransactionStatusChangedEvent; +import com.devpull.transactionservice.application.port.in.UpdateTransactionStatusUseCase; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; +import tools.jackson.databind.ObjectMapper; + +@Component +@RequiredArgsConstructor +@Slf4j +public class AntifraudResultListener { + + private final ObjectMapper objectMapper; + private final UpdateTransactionStatusUseCase useCase; + + @KafkaListener(topics = "${spring.kafka.topic.name}", groupId = "${spring.kafka.consumer.group-id}") + public void consume(byte[] payload) { + try{ + TransactionStatusChangedEvent event = + objectMapper.readValue(payload, TransactionStatusChangedEvent.class); + + log.info("Received antifraud event txId={}, status={}", + event.transactionId(), event.status()); + + useCase.updateStatus(event.transactionId(), event.status()) + .doOnError(e -> log.error("Failed updating tx {}", event.transactionId(), e)) + .subscribe(); + + } catch (Exception e) { + log.error("Failed to deserialize antifraud payload", e); + } + + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/KafkaEnableConfig.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/KafkaEnableConfig.java new file mode 100644 index 0000000000..146891f1bc --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/KafkaEnableConfig.java @@ -0,0 +1,9 @@ +package com.devpull.transactionservice.adapter.in.kafka; + +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; + +@Configuration +@EnableKafka +public class KafkaEnableConfig { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/TransactionStatusConsumer.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/TransactionStatusConsumer.java deleted file mode 100644 index d131ef4ace..0000000000 --- a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/TransactionStatusConsumer.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.devpull.transactionservice.adapter.in.kafka; - -import com.devpull.transactionservice.application.port.in.UpdateTransactionStatusUseCase; -import org.springframework.kafka.annotation.KafkaListener; -import org.springframework.stereotype.Component; - -import java.util.UUID; - -@Component -public class TransactionStatusConsumer { - - private final UpdateTransactionStatusUseCase useCase; - - public TransactionStatusConsumer(UpdateTransactionStatusUseCase useCase) { - this.useCase = useCase; - } - - @KafkaListener(topics = "${spring.kafka.topic.name}", groupId = "${spring.kafka.consumer.group-id}") - public void consume(TransactionStatusMessage message) { - useCase.updateStatus(message.transactionId(), message.status()) - .subscribe(); // subscribe to trigger the execution - } - - public record TransactionStatusMessage(UUID transactionId, String status) {} -} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/config/KafkaConsumerConfig.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/config/KafkaConsumerConfig.java new file mode 100644 index 0000000000..ffb129c02b --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/kafka/config/KafkaConsumerConfig.java @@ -0,0 +1,48 @@ +package com.devpull.transactionservice.adapter.in.kafka.config; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.serialization.ByteArrayDeserializer; +import org.apache.kafka.common.serialization.StringDeserializer; +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 java.util.HashMap; +import java.util.Map; + +@Configuration +@EnableKafka +public class KafkaConsumerConfig { + + @Bean + public ConsumerFactory consumerFactory( + @Value("${spring.kafka.bootstrap-servers}") String bootstrapServers, + @Value("${spring.kafka.consumer.group-id}") String groupId, + @Value("${spring.kafka.consumer.auto-offset-reset:earliest}") String autoOffsetReset + ) { + Map props = new HashMap<>(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ByteArrayDeserializer.class); + return new DefaultKafkaConsumerFactory<>(props); + } + + /** + * "kafkaListenerContainerFactory" + */ + @Bean(name = "kafkaListenerContainerFactory") + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory( + ConsumerFactory consumerFactory + ) { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory); + return factory; + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/kafka/TransactionStatusChangedEvent.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/kafka/TransactionStatusChangedEvent.java new file mode 100644 index 0000000000..0ed3c13dad --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/in/web/dto/kafka/TransactionStatusChangedEvent.java @@ -0,0 +1,11 @@ +package com.devpull.transactionservice.adapter.in.web.dto.kafka; + +import java.time.Instant; +import java.util.UUID; + +public record TransactionStatusChangedEvent( + UUID transactionId, + String status, + Instant evaluatedAt, + String reason +) {} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/AntiFraudWebClientAdapter.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/AntiFraudWebClientAdapter.java index b61ab01825..c2b5c16419 100644 --- a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/AntiFraudWebClientAdapter.java +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/AntiFraudWebClientAdapter.java @@ -1,5 +1,7 @@ package com.devpull.transactionservice.adapter.out.client; +import com.devpull.transactionservice.adapter.out.client.dto.TransactionCreatedEvent; +import com.devpull.transactionservice.adapter.out.client.mapper.AntiFraudEventMapper; import com.devpull.transactionservice.application.port.out.AntiFraudPort; import com.devpull.transactionservice.domain.model.Transaction; import org.springframework.stereotype.Component; @@ -17,9 +19,11 @@ public AntiFraudWebClientAdapter(WebClient webClient) { @Override public Mono send(Transaction tx) { + TransactionCreatedEvent event = AntiFraudEventMapper.toCreatedEvent(tx); + return webClient.post() - .uri("http://localhost:8081/api/antifraud") - .bodyValue(tx) + .uri("http://localhost:8081/api/v1/antifraud") + .bodyValue(event) .retrieve() .bodyToMono(String.class); } diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/TransactionCreatedEvent.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/TransactionCreatedEvent.java new file mode 100644 index 0000000000..a1ffbf0f7c --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/dto/TransactionCreatedEvent.java @@ -0,0 +1,12 @@ +package com.devpull.transactionservice.adapter.out.client.dto; + + +import java.util.UUID; + +public record TransactionCreatedEvent( + UUID transactionId, + UUID accountId, + String type, + double amount +) { +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/mapper/AntiFraudEventMapper.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/mapper/AntiFraudEventMapper.java new file mode 100644 index 0000000000..ad951468ba --- /dev/null +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/client/mapper/AntiFraudEventMapper.java @@ -0,0 +1,18 @@ +package com.devpull.transactionservice.adapter.out.client.mapper; + +import com.devpull.transactionservice.adapter.out.client.dto.TransactionCreatedEvent; +import com.devpull.transactionservice.domain.model.Transaction; + +public final class AntiFraudEventMapper { + + private AntiFraudEventMapper() {} + + public static TransactionCreatedEvent toCreatedEvent(Transaction tx) { + return new TransactionCreatedEvent( + tx.getId(), + tx.getAccountId(), + tx.getType().name(), + tx.getAmount() + ); + } +} diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/TransactionRepositoryAdapter.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/TransactionRepositoryAdapter.java index 09872e8735..315f0b2f36 100644 --- a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/TransactionRepositoryAdapter.java +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/adapter/TransactionRepositoryAdapter.java @@ -20,10 +20,17 @@ public TransactionRepositoryAdapter(R2dbcTransactionRepository repository) { @Override public Mono save(Transaction tx) { - return repository.save(TransactionPersistenceMapper.toEntity(tx)) + return repository.save(TransactionPersistenceMapper.toNewEntity(tx)) .map(TransactionPersistenceMapper::toDomain); } + @Override + public Mono update(Transaction tx) { + return repository.save(TransactionPersistenceMapper.toExistingEntity(tx)) + .map(TransactionPersistenceMapper::toDomain); + } + + @Override public Mono findById(UUID id) { return repository.findById(id) diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/AccountEntity.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/AccountEntity.java index f68525c486..ab7b71b3b4 100644 --- a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/AccountEntity.java +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/AccountEntity.java @@ -6,6 +6,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; +import org.springframework.data.domain.Persistable; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; @@ -16,7 +18,7 @@ @AllArgsConstructor @Getter @Table(name = "accounts") -public class AccountEntity { +public class AccountEntity implements Persistable { @Id private UUID id; @@ -33,4 +35,20 @@ public class AccountEntity { @Column("updated_at") private Instant updatedAt; + @Transient + private boolean isNew; + + public static AccountEntity newAccount(UUID id, AccountStatus status, AccountType type, Instant now) { + return new AccountEntity(id, status, type, now, now, true); + } + + public AccountEntity markNotNew() { + this.isNew = false; + return this; + } + + @Override + public boolean isNew() { + return isNew; + } } diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/TransactionEntity.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/TransactionEntity.java index 64539ac724..2a251bfbf0 100644 --- a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/TransactionEntity.java +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/entity/TransactionEntity.java @@ -6,6 +6,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import org.springframework.data.annotation.Id; +import org.springframework.data.annotation.Transient; +import org.springframework.data.domain.Persistable; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Table; @@ -16,7 +18,8 @@ @AllArgsConstructor @Getter @Table(name = "transactions") -public class TransactionEntity { +public class TransactionEntity implements Persistable { + @Id private UUID id; @@ -37,4 +40,24 @@ public class TransactionEntity { @Column("account_id") private UUID accountId; + + @Transient + private boolean isNew; + + public static TransactionEntity newTransaction(UUID id, TransactionStatus status, + TransactionType type, double amount, + UUID accountId, Instant now) { + return new TransactionEntity(id, status, type, amount, now, now, accountId, true); + } + + public TransactionEntity markNotNew() { + this.isNew = false; + return this; + } + + @Override + public boolean isNew() { + return isNew; + } + } diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/AccountPersistenceMapper.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/AccountPersistenceMapper.java index f673ccb2ae..e7d37507e6 100644 --- a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/AccountPersistenceMapper.java +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/AccountPersistenceMapper.java @@ -10,13 +10,10 @@ public final class AccountPersistenceMapper { private AccountPersistenceMapper() {} public static AccountEntity toEntity(Account domain) { - return new AccountEntity( - domain.getId(), - domain.getAccountStatus(), - domain.getAccountType(), - domain.getCreatedAt(), - Instant.now() // updated_at - ); + return AccountEntity.newAccount(domain.getId(), + domain.getAccountStatus(), + domain.getAccountType(), + Instant.now()); } public static Account toDomain(AccountEntity entity) { diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/TransactionPersistenceMapper.java b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/TransactionPersistenceMapper.java index 0c8359e1ba..fa48d8cbb6 100644 --- a/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/TransactionPersistenceMapper.java +++ b/transaction-service/src/main/java/com/devpull/transactionservice/adapter/out/persistence/mapper/TransactionPersistenceMapper.java @@ -3,19 +3,31 @@ import com.devpull.transactionservice.adapter.out.persistence.entity.TransactionEntity; import com.devpull.transactionservice.domain.model.Transaction; +import java.time.Instant; + public final class TransactionPersistenceMapper { private TransactionPersistenceMapper() {} - public static TransactionEntity toEntity(Transaction domain) { + public static TransactionEntity toNewEntity(Transaction domain) { + return TransactionEntity.newTransaction(domain.getId(), + domain.getStatus(), + domain.getType(), + domain.getAmount(), + domain.getAccountId(), + Instant.now()); + } + + public static TransactionEntity toExistingEntity(Transaction tx) { return new TransactionEntity( - domain.getId(), - domain.getStatus(), - domain.getType(), - domain.getAmount(), - domain.getCreatedAt(), - domain.getUpdatedAt(), - domain.getAccountId() + tx.getId(), + tx.getStatus(), + tx.getType(), + tx.getAmount(), + tx.getCreatedAt(), + tx.getUpdatedAt(), + tx.getAccountId(), + false ); } diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/TransactionRepositoryPort.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/TransactionRepositoryPort.java index 031dcf6608..37f10b4540 100644 --- a/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/TransactionRepositoryPort.java +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/port/out/TransactionRepositoryPort.java @@ -9,6 +9,8 @@ public interface TransactionRepositoryPort { Mono save(Transaction tx); + Mono update(Transaction tx); + Mono findById(UUID id); } diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateAccountService.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateAccountService.java index d78a3ae782..c5fab9c991 100644 --- a/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateAccountService.java +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/CreateAccountService.java @@ -32,8 +32,8 @@ public Mono createAccount(CreateAccountCommand command) { return Mono.error(new BadRequestException("accountType is required")); } - // Domain object creation - Account account = Account.createNew(command.accountStatus(), command.accountType()); + // Domain object creation + Account account = Account.createNew(command.accountStatus(), command.accountType()); // Persistence and mapping to result return accountRepositoryPort.save(account) diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/UpdateTransactionStatusService.java b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/UpdateTransactionStatusService.java index c914992cbf..9be91e2995 100644 --- a/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/UpdateTransactionStatusService.java +++ b/transaction-service/src/main/java/com/devpull/transactionservice/application/usecase/UpdateTransactionStatusService.java @@ -4,12 +4,14 @@ import com.devpull.transactionservice.application.port.out.TransactionRepositoryPort; import com.devpull.transactionservice.domain.enums.TransactionStatus; import com.devpull.transactionservice.domain.exception.ResourceNotFoundException; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; import java.util.UUID; @Service +@Slf4j public class UpdateTransactionStatusService implements UpdateTransactionStatusUseCase { private final TransactionRepositoryPort txPort; @@ -21,12 +23,12 @@ public UpdateTransactionStatusService(TransactionRepositoryPort txPort) { @Override public Mono updateStatus(UUID transactionId, String status) { TransactionStatus newStatus = TransactionStatus.valueOf(status.trim().toUpperCase()); - + log.info(":::::::: Updating transaction {} status to {}", transactionId, newStatus); return txPort.findById(transactionId) .switchIfEmpty(Mono.error(new ResourceNotFoundException("Transaction", "id", transactionId))) .flatMap(tx -> { tx.setStatus(newStatus); - return txPort.save(tx); + return txPort.update(tx); }) .then(); } diff --git a/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionStatus.java b/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionStatus.java index 89240e904e..f000182541 100644 --- a/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionStatus.java +++ b/transaction-service/src/main/java/com/devpull/transactionservice/domain/enums/TransactionStatus.java @@ -3,5 +3,5 @@ public enum TransactionStatus { PENDING, APPROVED, - REJECT + REJECTED } diff --git a/transaction-service/src/main/resources/application.yml b/transaction-service/src/main/resources/application.yml index 76361b6a85..fe2f5358d9 100644 --- a/transaction-service/src/main/resources/application.yml +++ b/transaction-service/src/main/resources/application.yml @@ -1,3 +1,7 @@ + +app: + boot-marker: "transaction-service-yml-loaded" + server: port: ${SERVER_PORT:8080} @@ -9,32 +13,29 @@ spring: web-application-type: reactive r2dbc: - url: ${R2DBC_URL:r2dbc:postgresql://localhost:5433/app-java-codechallenge} + url: ${R2DBC_URL:r2dbc:postgresql://localhost:5433/app_java_codechallenge} username: ${DB_USERNAME:postgres} password: ${DB_PASSWORD:postgres} kafka: bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} - producer: - key-serializer: org.apache.kafka.common.serialization.StringSerializer - value-serializer: org.springframework.kafka.support.serializer.JsonSerializer - consumer: group-id: ${KAFKA_CONSUMER_GROUP:transaction-service} auto-offset-reset: earliest key-deserializer: org.apache.kafka.common.serialization.StringDeserializer - value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer - properties: - spring: - json: - trusted: - packages: "*" + value-deserializer: org.apache.kafka.common.serialization.ByteArrayDeserializer + + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.apache.kafka.common.serialization.ByteArraySerializer topic: name: ${KAFKA_TOPIC_TRANSACTIONS:transactions_fraud} logging: level: - io.r2dbc.postgresql.QUERY: ${LOG_R2DBC_QUERY:INFO} - io.r2dbc.postgresql.PARAM: ${LOG_R2DBC_PARAM:INFO} + io.r2dbc.postgresql.QUERY: ${LOG_R2DBC_QUERY:DEBUG} + io.r2dbc.postgresql.PARAM: ${LOG_R2DBC_PARAM:DEBUG} + org.springframework.kafka: DEBUG + org.apache.kafka: INFO