From 88f230d4a62add93efdc76cdae3bc6abccb4cf5a Mon Sep 17 00:00:00 2001 From: James Carrillo Date: Wed, 18 Feb 2026 11:34:08 -0500 Subject: [PATCH] add micros for challenge@ --- .DS_Store | Bin 0 -> 6148 bytes API_TESTING_GUIDE.md | 67 ++++ HIGH_CONCURRENCY_STRATEGY.md | 151 +++++++++ ms-anti-fraud.yaml | 69 ++++ ms-anti-fraud/.DS_Store | Bin 0 -> 6148 bytes ms-anti-fraud/.gitattributes | 2 + ms-anti-fraud/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.properties | 3 + ms-anti-fraud/mvnw | 295 ++++++++++++++++++ ms-anti-fraud/mvnw.cmd | 189 +++++++++++ ms-anti-fraud/pom.xml | 92 ++++++ ms-anti-fraud/src/.DS_Store | Bin 0 -> 6148 bytes .../antifraud/MsAntiFraudApplication.java | 13 + .../port/in/ValidateTransactionUseCase.java | 9 + .../port/out/AntiFraudRulePort.java | 9 + .../out/TransactionStatusPublisherPort.java | 9 + .../service/ValidateTransactionService.java | 29 ++ .../command/TransactionValidationCommand.java | 8 + .../antifraud/domain/model/Transaction.java | 41 +++ .../in/TransactionCreatedKafkaConsumer.java | 38 +++ .../adapter/out/KafkaStatusUpdateAdapter.java | 40 +++ .../out/SimpleAntiFraudRuleAdapter.java | 32 ++ .../out/TransactionStatusUpdatedEvent.java | 8 + .../infrastructure/config/JacksonConfig.java | 19 ++ .../src/main/resources/application.yaml | 45 +++ .../MsAntiFraudApplicationTests.java | 13 + ms-transaction.yaml | 112 +++++++ ms-transaction/.DS_Store | Bin 0 -> 6148 bytes ms-transaction/.gitattributes | 2 + ms-transaction/.gitignore | 33 ++ .../.mvn/wrapper/maven-wrapper.properties | 3 + ms-transaction/mvnw | 295 ++++++++++++++++++ ms-transaction/mvnw.cmd | 189 +++++++++++ ms-transaction/pom.xml | 92 ++++++ ms-transaction/src/.DS_Store | Bin 0 -> 6148 bytes .../transaction/MsTransactionApplication.java | 13 + .../port/in/TransactionCreationUseCase.java | 9 + .../port/in/TransactionRetrievalUseCase.java | 11 + .../in/UpdateTransactionStatusUseCase.java | 9 + .../port/out/EventProducerPort.java | 8 + .../port/out/TransactionRepositoryPort.java | 16 + .../service/TransactionService.java | 43 +++ .../UpdateTransactionStatusService.java | 30 ++ .../transaction/domain/model/Transaction.java | 41 +++ .../TransactionStatusKafkaConsumer.java | 44 +++ .../TransactionStatusUpdatedEvent.java | 9 + .../in/rest/TransactionController.java | 55 ++++ .../in/rest/dto/TransactionRequest.java | 10 + .../in/rest/dto/TransactionResponse.java | 32 ++ .../adapter/out/JpaTransactionAdapter.java | 71 +++++ .../adapter/out/KafkaEventAdapter.java | 37 +++ .../adapter/out/TransactionEntity.java | 61 ++++ .../adapter/out/TransactionRepository.java | 14 + .../config/ApplicationConfig.java | 20 ++ .../src/main/resources/application.yaml | 43 +++ .../MsTransactionApplicationTests.java | 13 + .../transaction/TransactionServiceTest.java | 123 ++++++++ 57 files changed, 2652 insertions(+) create mode 100644 .DS_Store create mode 100644 API_TESTING_GUIDE.md create mode 100644 HIGH_CONCURRENCY_STRATEGY.md create mode 100644 ms-anti-fraud.yaml create mode 100644 ms-anti-fraud/.DS_Store create mode 100644 ms-anti-fraud/.gitattributes create mode 100644 ms-anti-fraud/.gitignore create mode 100644 ms-anti-fraud/.mvn/wrapper/maven-wrapper.properties create mode 100755 ms-anti-fraud/mvnw create mode 100644 ms-anti-fraud/mvnw.cmd create mode 100644 ms-anti-fraud/pom.xml create mode 100644 ms-anti-fraud/src/.DS_Store create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/MsAntiFraudApplication.java create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/application/port/in/ValidateTransactionUseCase.java create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/application/port/out/AntiFraudRulePort.java create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/application/port/out/TransactionStatusPublisherPort.java create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/application/service/ValidateTransactionService.java create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/domain/command/TransactionValidationCommand.java create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/domain/model/Transaction.java create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/adapter/in/TransactionCreatedKafkaConsumer.java create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/adapter/out/KafkaStatusUpdateAdapter.java create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/adapter/out/SimpleAntiFraudRuleAdapter.java create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/adapter/out/TransactionStatusUpdatedEvent.java create mode 100644 ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/config/JacksonConfig.java create mode 100644 ms-anti-fraud/src/main/resources/application.yaml create mode 100644 ms-anti-fraud/src/test/java/com/jotace/antifraud/MsAntiFraudApplicationTests.java create mode 100644 ms-transaction.yaml create mode 100644 ms-transaction/.DS_Store create mode 100644 ms-transaction/.gitattributes create mode 100644 ms-transaction/.gitignore create mode 100644 ms-transaction/.mvn/wrapper/maven-wrapper.properties create mode 100755 ms-transaction/mvnw create mode 100644 ms-transaction/mvnw.cmd create mode 100644 ms-transaction/pom.xml create mode 100644 ms-transaction/src/.DS_Store create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/MsTransactionApplication.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/application/port/in/TransactionCreationUseCase.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/application/port/in/TransactionRetrievalUseCase.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/application/port/in/UpdateTransactionStatusUseCase.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/application/port/out/EventProducerPort.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/application/port/out/TransactionRepositoryPort.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/application/service/TransactionService.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/application/service/UpdateTransactionStatusService.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/domain/model/Transaction.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/consumer/TransactionStatusKafkaConsumer.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/consumer/TransactionStatusUpdatedEvent.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/TransactionController.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/dto/TransactionRequest.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/dto/TransactionResponse.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/JpaTransactionAdapter.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/KafkaEventAdapter.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/TransactionEntity.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/TransactionRepository.java create mode 100644 ms-transaction/src/main/java/com/jotace/transaction/infrastructure/config/ApplicationConfig.java create mode 100644 ms-transaction/src/main/resources/application.yaml create mode 100644 ms-transaction/src/test/java/com/jotace/transaction/MsTransactionApplicationTests.java create mode 100644 ms-transaction/src/test/java/com/jotace/transaction/TransactionServiceTest.java diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5b97d0825bf85f7c715585702a69e2fe5742e8c5 GIT binary patch literal 6148 zcmeHKy-veG4EBW%LHwvA$tcbnX8(z>)#}K6of&qF4%sPX|mm0svcZ2f;O;B{;_`CW@sX zMj%d7fs*RN@cR%>E)g_q3>X7-2Daq3&HexQ z`}2Q2$?l8+W8hyg;96-f?V=>@t&QTi*LvtVl!fC;!DR|ALMuirx8eh65coY`029Sh R5FUvA2m~6;7y}z+;2jocW>f$G literal 0 HcmV?d00001 diff --git a/API_TESTING_GUIDE.md b/API_TESTING_GUIDE.md new file mode 100644 index 0000000000..f6ffef8bab --- /dev/null +++ b/API_TESTING_GUIDE.md @@ -0,0 +1,67 @@ +# API Testing Guide + +### Cómo probar las APIs del proyecto + +Este documento describe paso a paso cómo probar los endpoints del microservicio **ms-transaction**, incluyendo la creación de transacciones, validación por el microservicio anti-fraud y la consulta de transacciones por su ID externo. + +Se asume que ambos microservicios están levantados localmente y que Kafka está funcionando según la configuración provista. + +--- + +# 1. Crear una transacción (caso aprobado) + +Este endpoint crea una transacción con estado inicial **pending**, la envía a Kafka, y posteriormente el microservicio **ms-anti-fraud** actualizará su estado a **approved** si el valor es menor o igual a 1000. + +### **Request:** + +```bash +curl --location 'http://localhost:8081/transaction' \ +--header 'Content-Type: application/json' \ +--data '{ + "accountExternalIdDebit": "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", + "accountExternalIdCredit": "b1c2d3e4-f5a6-7b8c-9d0e-1f2a3b4c5d6e", + "tranferTypeId": 1, + "value": 500.50 +}' +``` + +### **Respuesta esperada (inmediata):** + +```json +{ + "transactionExternalId": "GENERATED-ID", + "transactionType": { "name": "transfer" }, + "transactionStatus": { "name": "pending" }, + "value": 500.50, + "createdAt": "2025-12-05T..." +} +``` + +> El estado cambiará a **approved** después de que ms-anti-fraud procese el mensaje Kafka. + +--- + +# 2. Crear una transacción (caso rechazado) + +Si el valor es **mayor a 1000**, el anti-fraud debe actualizar la transacción a **rejected**. + +### **Request:** + +```bash +curl --location 'http://localhost:8081/transaction' \ +--header 'Content-Type: application/json' \ +--data '{ + "accountExternalIdDebit": "f0eebc99-9c0b-4ef8-bb6d-6bb9bd380a22", + "accountExternalIdCredit": "c1d2e3f4-a5b6-c7d8-e9f0-1a2b3c4d5e6f", + "tranferTypeId": 1, + "value": 1500.00 +}' +``` + +### **Respuesta inicial:** + +```json +{ + "transactionExternalId": "GENERATED-ID", + "transactionType": { "name": "transfer" +``` diff --git a/HIGH_CONCURRENCY_STRATEGY.md b/HIGH_CONCURRENCY_STRATEGY.md new file mode 100644 index 0000000000..e6432ae878 --- /dev/null +++ b/HIGH_CONCURRENCY_STRATEGY.md @@ -0,0 +1,151 @@ +# High Concurrency Strategy + +### Cómo escalar la solución en escenarios de alto volumen de transacciones + +Este documento explica las estrategias recomendadas para soportar escenarios con **alto volumen de lectura y escritura simultánea**, manteniendo consistencia, resiliencia y buen rendimiento. Todas las propuestas están alineadas con la arquitectura actual del proyecto (Java, Kafka, microservicios con enfoque hexagonal). + +--- + +## 1. Uso de Kafka para desacoplamiento y absorción de carga + +Kafka actúa como **buffer natural** entre ms-transaction y ms-anti-fraud. + +Beneficios en alta concurrencia: + +* Maneja millones de mensajes por segundo. +* Garantiza procesamiento secuencial por partición. +* Permite escalar consumidores horizontalmente. +* Evita que la carga de validación impacte directamente al microservicio de creación. + +**Estrategia propuesta:** + +* Aumentar particiones en el topic `transaction-created`. +* Habilitar múltiples instancias del anti-fraud service como **consumer group**, cada una procesando particiones diferentes. +* Usar claves (key = `transactionExternalId`) para mantener orden en mensajes relacionados a una misma transacción. + +--- + +## 2. Idempotencia en actualización de estado + +En escenarios de alto volumen, pueden generarse eventos duplicados o llegados en desorden. + +**Estrategia propuesta:** + +* La actualización de transacción debe ser **idempotente**: + si llega un mensaje con el mismo `transactionExternalId` y el mismo estado, la operación no debe causar inconsistencias. +* El repositorio JPA debe buscar primero por `transactionExternalId` y solo actualizar si el estado cambió realmente. + +Esto ya se cumple parcialmente en la solución implementada, pero se documenta como fundamento técnico. + +--- + +## 3. Optimistic Locking en la base de datos + +Para evitar condiciones de carrera al actualizar el estado de una transacción: + +**Estrategia propuesta:** + +* Agregar un campo `@Version` en la entidad JPA (`@Version Long version;`) +* Esto evita que dos consumidores actualicen simultáneamente la misma fila causando inconsistencias. +* Si ocurre un conflicto, JPA lanza exception y se puede reintentar (retry simple o backoff exponencial). + +--- + +## 4. Índices en columnas críticas + +Al aumentar la carga, el tiempo de consulta puede crecer si la base no está optimizada. + +**Recomendación:** + +* Crear índices en: + + * `transactionExternalId` + * `createdAt` + * `transactionStatus` + +Esto acelera: + +* búsquedas puntuales (GET transaction) +* actualizaciones basadas en ID externo + +--- + +## 5. Segmentación de carga por microservicios + +La arquitectura ya contempla separación entre: + +* microservicio de creación (**ms-transaction**) +* microservicio de validación (**ms-anti-fraud**) + +Para escenarios de alta concurrencia: + +* Escalar **independientemente** el ms-transaction cuando crecen las escrituras. +* Escalar **independientemente** el anti-fraud cuando crece la validación. +* Kafka absorbe el desacople sin saturar ninguno de los dos. + +--- + +## 6. Cache para lecturas intensivas (opcional) + +Si aumenta mucho el volumen de lecturas (GET transaction), la base de datos puede convertirse en cuello de botella. + +**Solución sugerida (opcional):** + +* Cache para transacciones recién creadas o consultadas (Redis) +* Expiración corta (TTL 30-60s) + +Esto reduce hasta el 80% de lecturas a DB. + +--- + +## 7. Failover y resiliencia de eventos + +Para asegurar entrega confiable: + +* Habilitar **retries** en el producer de Kafka. +* Configurar **dead-letter-topic** para eventos que no se puedan procesar. +* Asegurar que los consumidores sean **at-least-once delivery** (la solución actual ya lo cumple). + +--- + +## 8. Consistencia eventual y diseño orientado a eventos + +El sistema funciona bajo un patrón de **eventual consistency**: + +* La transacción se crea como `pending` +* Anti-fraud responde asíncronamente +* ms-transaction actualiza estado cuando recibe el evento + +Esto es ideal para alta concurrencia porque: + +* No bloquea peticiones sincronamente +* Mantiene alto throughput +* Reduce latencia percibida por el usuario + +--- + +## 9. Monitoreo y Observabilidad + +Para ambientes productivos se recomienda: + +* Métricas de Kafka (lag, throughput) +* Métricas de JPA (tiempos de consulta) +* Dashboards de estado de consumidores +* Alertas por retraso en procesamiento de particiones + +Esto permite anticipar saturación y escalar antes de fallar. + +--- + +# Conclusión + +La solución propuesta soporta carga alta gracias al diseño basado en eventos, el desacoplamiento con Kafka, y la separación de responsabilidades entre microservicios. + +Para escenarios extremos, las estrategias detalladas permiten: + +* escalar horizontalmente ambos servicios, +* procesar millones de eventos de forma ordenada e idempotente, +* evitar condiciones de carrera, +* y optimizar las lecturas en tiempo real. + +Este documento complementa la entrega técnica sin necesidad de modificar la implementación actual. \ No newline at end of file diff --git a/ms-anti-fraud.yaml b/ms-anti-fraud.yaml new file mode 100644 index 0000000000..c7a907417d --- /dev/null +++ b/ms-anti-fraud.yaml @@ -0,0 +1,69 @@ +openapi: 3.0.3 +info: + title: Anti-Fraud Service (Event-Driven API) + description: > + Anti-Fraud microservice responsible for processing financial transactions received via Kafka, + validating fraud rules, and publishing updated transaction statuses. + version: 1.0.0 + +servers: + - url: kafka://localhost:9092 + description: Local Kafka Broker + +paths: {} + +components: + schemas: + TransactionCreatedEvent: + type: object + required: + - transactionExternalId + - accountExternalIdDebit + - accountExternalIdCredit + - tranferTypeId + - value + - createdAt + properties: + transactionExternalId: + type: string + format: uuid + accountExternalIdDebit: + type: string + format: uuid + accountExternalIdCredit: + type: string + format: uuid + tranferTypeId: + type: integer + value: + type: number + format: double + createdAt: + type: string + format: date-time + + TransactionStatusUpdatedEvent: + type: object + required: + - transactionExternalId + - transactionStatus + properties: + transactionExternalId: + type: string + format: uuid + transactionStatus: + type: string + enum: + - approved + - rejected + +x-kafka-topics: + transaction-created: + description: Event published by ms-transaction when a new transaction is created. + message: + $ref: '#/components/schemas/TransactionCreatedEvent' + + transaction-status-updated: + description: Event published by ms-anti-fraud after fraud evaluation. + message: + $ref: '#/components/schemas/TransactionStatusUpdatedEvent' diff --git a/ms-anti-fraud/.DS_Store b/ms-anti-fraud/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..b1f02402af42bf5783a8e50461f396af671cd39f GIT binary patch literal 6148 zcmeHKOHRWu5FNKc3W7zKNU-n;ka_|_6*vIB0Q4h1YDg)fY<&vOz={*FXUhrLa{<=8 z@mOt~6tP1QnvupY8PDV;FOD4(kr_Np`b0e`=4=dQRew*R*n!&pH5$H7R$HeddB3JjQLe{ggBH_ z0{()QbXyxe&(*#vH{y@)cbBPGB}+hhjWC;?}2=$pp1k1yq4p zfv%kPc>O>5dH)|5>6t2^3j8YtOp*`t0Ujx=t&NA{wbsJV;cT4e1Xm?E@Jlgrc`4q5 adqdpv0Wb^b1QCJhkARm!8&%*>75D~~FJ37C literal 0 HcmV?d00001 diff --git a/ms-anti-fraud/.gitattributes b/ms-anti-fraud/.gitattributes new file mode 100644 index 0000000000..3b41682ac5 --- /dev/null +++ b/ms-anti-fraud/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/ms-anti-fraud/.gitignore b/ms-anti-fraud/.gitignore new file mode 100644 index 0000000000..667aaef0c8 --- /dev/null +++ b/ms-anti-fraud/.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/ms-anti-fraud/.mvn/wrapper/maven-wrapper.properties b/ms-anti-fraud/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..c0bcafe984 --- /dev/null +++ b/ms-anti-fraud/.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.11/apache-maven-3.9.11-bin.zip diff --git a/ms-anti-fraud/mvnw b/ms-anti-fraud/mvnw new file mode 100755 index 0000000000..bd8896bf22 --- /dev/null +++ b/ms-anti-fraud/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/ms-anti-fraud/mvnw.cmd b/ms-anti-fraud/mvnw.cmd new file mode 100644 index 0000000000..92450f9327 --- /dev/null +++ b/ms-anti-fraud/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/ms-anti-fraud/pom.xml b/ms-anti-fraud/pom.xml new file mode 100644 index 0000000000..f8c43d9598 --- /dev/null +++ b/ms-anti-fraud/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.9-SNAPSHOT + + + com.jotace + ms-anti-fraud + 0.0.1-SNAPSHOT + ms-anti-fraud + Ms Anti-Fraud + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.kafka + spring-kafka + + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.kafka + spring-kafka-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + diff --git a/ms-anti-fraud/src/.DS_Store b/ms-anti-fraud/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..50da90a5d02acce1710e85f82a7ccd4b3e300ae8 GIT binary patch literal 6148 zcmeHKOHKnZ47FhvmDqI2GAs7IL8yik^a4YqKIwo!eQ8U%WZi6fF@$f5+Sgq zz-p7T0~Z8Cm4Fn&jCV>gbB@ z>7jo9vzMW->)Up@?w7Dh%=3QxdS3fi-}*OqLvHp6Mcy6V&?D{Yy!1Bo9n9;oU4J02 z`|G^&okN0wU?3O>2L3q%sM#Wu6T|3(fnXpQ_+&u#hlD1W9gCrE9q9B50Bm4Zfv#sv zU=jl`I~GG&AZ(#P3uV7zu!X~(d|q}eh89k&nX!&%{9GeCBF`dQ7{k;{4oZ&X&3DrFXgrM%j-$4O=y>BA~LUt0)eij7^tNf1KCGT d)oK1DI{ewO7|JSQ*KlAQ1eB2If`K1k;2jUhK`H kafkaTemplate; + + public KafkaStatusUpdateAdapter(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Override + public void publishStatusUpdate(UUID transactionExternalId, String newStatus) { + + TransactionStatusUpdatedEvent event = + new TransactionStatusUpdatedEvent(transactionExternalId, newStatus); + + try { + kafkaTemplate.send(TOPIC, transactionExternalId.toString(), event); + + log.info("Status update published | ID: {} | Status: {}", + transactionExternalId, newStatus); + + } catch (Exception e) { + log.error("Failed to send status update event for ID: {}", transactionExternalId, e); + throw new IllegalStateException("Kafka publishing failed.", e); + } + } + +} diff --git a/ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/adapter/out/SimpleAntiFraudRuleAdapter.java b/ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/adapter/out/SimpleAntiFraudRuleAdapter.java new file mode 100644 index 0000000000..5b65d92287 --- /dev/null +++ b/ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/adapter/out/SimpleAntiFraudRuleAdapter.java @@ -0,0 +1,32 @@ +package com.jotace.antifraud.infrastructure.adapter.out; + +import com.jotace.antifraud.application.port.out.AntiFraudRulePort; +import com.jotace.antifraud.domain.command.TransactionValidationCommand; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class SimpleAntiFraudRuleAdapter implements AntiFraudRulePort { + + private static final Logger log = LoggerFactory.getLogger(SimpleAntiFraudRuleAdapter.class); + + private static final double MAX_ALLOWED_VALUE = 1000.0; + + @Override + public String applyRule(TransactionValidationCommand command) { + String status; + + if (command.value() > MAX_ALLOWED_VALUE) { + status = "rejected"; + log.warn("Anti-Fraud Rule triggered: Transaction {} REJECTED. Value: {}", + command.transactionExternalId(), command.value()); + } else { + status = "approved"; + log.info("Transaction {} APPROVED by Anti-Fraud Rule. Value: {}", + command.transactionExternalId(), command.value()); + } + + return status; + } +} diff --git a/ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/adapter/out/TransactionStatusUpdatedEvent.java b/ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/adapter/out/TransactionStatusUpdatedEvent.java new file mode 100644 index 0000000000..0964619c37 --- /dev/null +++ b/ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/adapter/out/TransactionStatusUpdatedEvent.java @@ -0,0 +1,8 @@ +package com.jotace.antifraud.infrastructure.adapter.out; + +import java.util.UUID; + +public record TransactionStatusUpdatedEvent( + UUID transactionExternalId, + String newStatus +) {} diff --git a/ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/config/JacksonConfig.java b/ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/config/JacksonConfig.java new file mode 100644 index 0000000000..bcdd89d1e0 --- /dev/null +++ b/ms-anti-fraud/src/main/java/com/jotace/antifraud/infrastructure/config/JacksonConfig.java @@ -0,0 +1,19 @@ +package com.jotace.antifraud.infrastructure.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JacksonConfig { + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper() + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + } + +} diff --git a/ms-anti-fraud/src/main/resources/application.yaml b/ms-anti-fraud/src/main/resources/application.yaml new file mode 100644 index 0000000000..c7b95b6fad --- /dev/null +++ b/ms-anti-fraud/src/main/resources/application.yaml @@ -0,0 +1,45 @@ +spring: + application: + name: ms-anti-fraud + + datasource: + url: jdbc:postgresql://localhost:5432/transaction_db + username: postgres + password: postgres + driver-class-name: org.postgresql.Driver + + jpa: + show-sql: true + hibernate: + ddl-auto: update + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: true + open-in-view: false + + kafka: + bootstrap-servers: localhost:9092 + + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + properties: + spring.json.use.type.headers: false + spring.json.add.type.headers: false + spring.json.trusted.packages: "*" + acks: all + retries: 10 + + consumer: + group-id: anti-fraud-group + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + spring.json.value.default.type: com.jotace.antifraud.domain.model.Transaction + spring.json.use.type.headers: false + spring.json.add.type.headers: false + +server: + port: 8082 \ No newline at end of file diff --git a/ms-anti-fraud/src/test/java/com/jotace/antifraud/MsAntiFraudApplicationTests.java b/ms-anti-fraud/src/test/java/com/jotace/antifraud/MsAntiFraudApplicationTests.java new file mode 100644 index 0000000000..c7fcfb2855 --- /dev/null +++ b/ms-anti-fraud/src/test/java/com/jotace/antifraud/MsAntiFraudApplicationTests.java @@ -0,0 +1,13 @@ +package com.jotace.antifraud; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class MsAntiFraudApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/ms-transaction.yaml b/ms-transaction.yaml new file mode 100644 index 0000000000..fa6f6e2e32 --- /dev/null +++ b/ms-transaction.yaml @@ -0,0 +1,112 @@ +openapi: 3.0.3 +info: + title: Transaction Service API + description: API for creating and retrieving financial transactions. + version: 1.0.0 + +servers: + - url: http://localhost:8081 + description: Local Development + +paths: + /transaction: + post: + summary: Create a new transaction + description: | + Creates a new transaction with status *pending*, publishes the event to Kafka, + and returns the stored transaction. + The final status (*approved* or *rejected*) will be updated asynchronously by the anti-fraud service. + operationId: createTransaction + tags: + - Transactions + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionRequest' + example: + accountExternalIdDebit: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" + accountExternalIdCredit: "b1c2d3e4-f5a6-7b8c-9d0e-1f2a3b4c5d6e" + tranferTypeId: 1 + value: 500.50 + responses: + '201': + description: Transaction created successfully + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionResponse' + '400': + description: Invalid input + + /transaction/{transactionExternalId}: + get: + summary: Get a transaction by its external ID + description: Retrieve a single transaction including its current status. + operationId: getTransactionById + tags: + - Transactions + parameters: + - name: transactionExternalId + in: path + required: true + schema: + type: string + format: uuid + description: UUID of the transaction to retrieve. + responses: + '200': + description: Transaction found + content: + application/json: + schema: + $ref: '#/components/schemas/TransactionResponse' + '404': + description: Transaction not found + +components: + schemas: + TransactionRequest: + type: object + required: + - accountExternalIdDebit + - accountExternalIdCredit + - tranferTypeId + - value + properties: + accountExternalIdDebit: + type: string + format: uuid + accountExternalIdCredit: + type: string + format: uuid + tranferTypeId: + type: integer + value: + type: number + format: double + + TransactionResponse: + type: object + properties: + transactionExternalId: + type: string + format: uuid + transactionType: + type: object + properties: + name: + type: string + transactionStatus: + type: object + properties: + name: + type: string + enum: [pending, approved, rejected] + value: + type: number + format: double + createdAt: + type: string + format: date-time diff --git a/ms-transaction/.DS_Store b/ms-transaction/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..96d1895e0e19a5b82c56b9f5f8b34488696a8a2a GIT binary patch literal 6148 zcmeHKO-jQ+6n@i2sOX|gmk~Fvgx(-6@dEY&+D24pOjQ&FksQG@xbhrsT)J@Y0lYxB ze(${@%_J3fQsh0D`6lltANexmWr)a)ZnFVVpNM)GjGawPD~#hjTGsKNZJ?8ToXpM! z<7tu4Te;>~PX+khZO{Q-P)4WJcz^r%VKgt&bTTU@@b-q!yI&s^IGJd*FNt%u`Y8)F<}uyJ0gSe0NQtr)r7 digz%?;E#C$Ogxr~@WAXxKxDAV6!=pGz5(96V1WPt literal 0 HcmV?d00001 diff --git a/ms-transaction/.gitattributes b/ms-transaction/.gitattributes new file mode 100644 index 0000000000..3b41682ac5 --- /dev/null +++ b/ms-transaction/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/ms-transaction/.gitignore b/ms-transaction/.gitignore new file mode 100644 index 0000000000..667aaef0c8 --- /dev/null +++ b/ms-transaction/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/ms-transaction/.mvn/wrapper/maven-wrapper.properties b/ms-transaction/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..c0bcafe984 --- /dev/null +++ b/ms-transaction/.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.11/apache-maven-3.9.11-bin.zip diff --git a/ms-transaction/mvnw b/ms-transaction/mvnw new file mode 100755 index 0000000000..bd8896bf22 --- /dev/null +++ b/ms-transaction/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/ms-transaction/mvnw.cmd b/ms-transaction/mvnw.cmd new file mode 100644 index 0000000000..92450f9327 --- /dev/null +++ b/ms-transaction/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/ms-transaction/pom.xml b/ms-transaction/pom.xml new file mode 100644 index 0000000000..a321bf1caa --- /dev/null +++ b/ms-transaction/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.9-SNAPSHOT + + + com.jotace + ms-transaction + 0.0.1-SNAPSHOT + ms-transaction + Ms transaction + + + + + + + + + + + + + + + 21 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.kafka + spring-kafka + + + + org.postgresql + postgresql + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.kafka + spring-kafka-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + \ No newline at end of file diff --git a/ms-transaction/src/.DS_Store b/ms-transaction/src/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..12eb19a082e8b8ad0e32a194978be7e708ce62b8 GIT binary patch literal 6148 zcmeHKJ5Iwu5S;-Rk!aFHqTCxmZeSv(q(Hd?e75M?`e?yqb!PMP!6QR4M~P^Pua%gXvVRvA&%yuA6!@u*h#U@!bolM0<*1x&yaHAzsQFkjh@+8}S-N|{^x3{k6*3F)} z?(fx=Q`QX$27-ZLAQ<@H4B*VBjE)Rr4hDjOVBnJh?hgrrSUR?bdF#NGTL54Ia~A0A zC76>OOUKp_9*9~h&{EY~47GIFlgE{gt)Zohdh?-P`J;K!TpjDD94>a#Ha+HGAQ;#& zaA@k7=l>kP%wUth3lA+A2nPNj16ovzYK9N findByExternalId(UUID transactionExternalId); + + void updateStatus(UUID transactionExternalId, String newStatus); + +} diff --git a/ms-transaction/src/main/java/com/jotace/transaction/application/service/TransactionService.java b/ms-transaction/src/main/java/com/jotace/transaction/application/service/TransactionService.java new file mode 100644 index 0000000000..06a4eb207a --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/application/service/TransactionService.java @@ -0,0 +1,43 @@ +package com.jotace.transaction.application.service; + +import com.jotace.transaction.application.port.in.TransactionCreationUseCase; +import com.jotace.transaction.application.port.in.TransactionRetrievalUseCase; +import com.jotace.transaction.application.port.out.EventProducerPort; +import com.jotace.transaction.application.port.out.TransactionRepositoryPort; +import com.jotace.transaction.domain.model.Transaction; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +public class TransactionService implements TransactionCreationUseCase, TransactionRetrievalUseCase { + + // Injecting Output Ports (depends on contracts, not implementations) + private final TransactionRepositoryPort transactionRepositoryPort; + private final EventProducerPort eventProducer; + + public TransactionService(TransactionRepositoryPort transactionRepositoryPort, EventProducerPort eventProducer) { + this.transactionRepositoryPort = transactionRepositoryPort; + this.eventProducer = eventProducer; + } + + @Override + public Transaction createTransaction(String accountExternalIdDebit, String accountExternalIdCredit, int transferTypeId, double value) { + // 1. Create the domain entity with initial state 'pending' + Transaction newTransaction = new Transaction(accountExternalIdDebit, accountExternalIdCredit, transferTypeId, value); + + // 2. Persist the entity into the database (using the TransactionRepositoryPort) + Transaction savedTransaction = transactionRepositoryPort.save(newTransaction); + + // 3. Publish the event to Kafka (using the EventProducerPort) + eventProducer.sendTransactionCreatedEvent(savedTransaction); + + // 4. Return the created transaction back to the REST adapter + return savedTransaction; + } + + @Override + public Transaction getTransactionById(UUID uuid) { + return transactionRepositoryPort.findByExternalId(uuid).orElse(null); + } +} diff --git a/ms-transaction/src/main/java/com/jotace/transaction/application/service/UpdateTransactionStatusService.java b/ms-transaction/src/main/java/com/jotace/transaction/application/service/UpdateTransactionStatusService.java new file mode 100644 index 0000000000..d21423ccd6 --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/application/service/UpdateTransactionStatusService.java @@ -0,0 +1,30 @@ +package com.jotace.transaction.application.service; + +import com.jotace.transaction.application.port.in.UpdateTransactionStatusUseCase; +import com.jotace.transaction.application.port.out.TransactionRepositoryPort; +import jakarta.transaction.Transactional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +public class UpdateTransactionStatusService implements UpdateTransactionStatusUseCase { + + private static final Logger log = LoggerFactory.getLogger(UpdateTransactionStatusService.class); + + private final TransactionRepositoryPort repository; + + public UpdateTransactionStatusService(TransactionRepositoryPort repository) { + this.repository = repository; + } + + @Transactional + @Override + public void updateStatus(UUID externalId, String newStatus) { + repository.updateStatus(externalId, newStatus); + log.info("Updated transaction for external id {} and status {}", externalId, newStatus); + } + +} diff --git a/ms-transaction/src/main/java/com/jotace/transaction/domain/model/Transaction.java b/ms-transaction/src/main/java/com/jotace/transaction/domain/model/Transaction.java new file mode 100644 index 0000000000..6745d042a6 --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/domain/model/Transaction.java @@ -0,0 +1,41 @@ +package com.jotace.transaction.domain.model; + +import java.time.Instant; +import java.util.UUID; + +public record Transaction( + UUID transactionExternalId, + String accountExternalIdDebit, + String accountExternalIdCredit, + int tranferTypeId, + double value, + String transactionStatus, // State field: tracks 'pending', 'approved', 'rejected'. + Instant createdAt +) { + // Constructor for creating a new Transaction. Initializes UUID, timestamp, and "pending" status. + public Transaction(String accountExternalIdDebit, String accountExternalIdCredit, int tranferTypeId, double value) { + this( + UUID.randomUUID(), + accountExternalIdDebit, + accountExternalIdCredit, + tranferTypeId, + value, + "pending", + Instant.now() + ); + } + + // Creates a NEW INSTANCE of the Record with the specified status. + // Preserves immutability by returning a copy with the updated state field. + public Transaction updateStatus(String newStatus) { + return new Transaction( + this.transactionExternalId(), + this.accountExternalIdDebit(), + this.accountExternalIdCredit(), + this.tranferTypeId(), + this.value(), + newStatus, + this.createdAt() + ); + } +} \ No newline at end of file diff --git a/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/consumer/TransactionStatusKafkaConsumer.java b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/consumer/TransactionStatusKafkaConsumer.java new file mode 100644 index 0000000000..2df1f83c83 --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/consumer/TransactionStatusKafkaConsumer.java @@ -0,0 +1,44 @@ +package com.jotace.transaction.infrastructure.adapter.in.consumer; + +import com.jotace.transaction.application.port.in.UpdateTransactionStatusUseCase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +public class TransactionStatusKafkaConsumer { + + private static final Logger log = LoggerFactory.getLogger(TransactionStatusKafkaConsumer.class); + + private final UpdateTransactionStatusUseCase updateTransactionStatusUseCase; + + public TransactionStatusKafkaConsumer(UpdateTransactionStatusUseCase updateTransactionStatusUseCase) { + this.updateTransactionStatusUseCase = updateTransactionStatusUseCase; + } + + @KafkaListener(topics = "transaction-status-updated", groupId = "transaction-status-group") + public void listen(TransactionStatusUpdatedEvent event) { + + try { + + log.info("Received status update | ID: {} | newStatus: {}", + event.transactionExternalId(), event.newStatus()); + + try { + + updateTransactionStatusUseCase.updateStatus( + event.transactionExternalId(), + event.newStatus() + ); + + } catch (Exception e) { + log.error("Error processing transaction-status-updated event", e); + } + + } catch (Exception e) { + log.error("Error processing transaction-status-updated event", e); + } + } + +} diff --git a/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/consumer/TransactionStatusUpdatedEvent.java b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/consumer/TransactionStatusUpdatedEvent.java new file mode 100644 index 0000000000..158b8d466b --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/consumer/TransactionStatusUpdatedEvent.java @@ -0,0 +1,9 @@ +package com.jotace.transaction.infrastructure.adapter.in.consumer; + +import java.util.UUID; + +public record TransactionStatusUpdatedEvent( + UUID transactionExternalId, + String newStatus +) {} + diff --git a/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/TransactionController.java b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/TransactionController.java new file mode 100644 index 0000000000..4ef432b548 --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/TransactionController.java @@ -0,0 +1,55 @@ +package com.jotace.transaction.infrastructure.adapter.in.rest; + +import com.jotace.transaction.application.port.in.TransactionCreationUseCase; +import com.jotace.transaction.application.port.in.TransactionRetrievalUseCase; +import com.jotace.transaction.domain.model.Transaction; +import com.jotace.transaction.infrastructure.adapter.in.rest.dto.TransactionRequest; +import com.jotace.transaction.infrastructure.adapter.in.rest.dto.TransactionResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/transaction") +public class TransactionController { + + private final TransactionCreationUseCase transactionCreationUseCase; + private final TransactionRetrievalUseCase transactionRetrievalUseCase; + + // Inject the necessary Use Case(s) + public TransactionController(TransactionCreationUseCase transactionCreationUseCase, + TransactionRetrievalUseCase transactionRetrievalUseCase) { + this.transactionCreationUseCase = transactionCreationUseCase; + this.transactionRetrievalUseCase = transactionRetrievalUseCase; + } + + // POST /transaction (Creation Adapter) - Single Responsibility: HTTP to UseCase mapping + @PostMapping + public ResponseEntity createTransaction(@RequestBody TransactionRequest request) { + try { + Transaction transaction = transactionCreationUseCase.createTransaction( + request.accountExternalIdDebit(), + request.accountExternalIdCredit(), + request.tranferTypeId(), // Corrected field access + request.value() + ); + + TransactionResponse response = TransactionResponse.fromDomain(transaction); + + return new ResponseEntity<>(response, HttpStatus.CREATED); + } catch (Exception e) { + // Generic error handling (e.g., database or Kafka failure) + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + // GET /transaction/{id} (Retrieval Adapter) + @GetMapping("/{id}") + public ResponseEntity getTransactionById(@PathVariable("id") UUID id) { + Transaction transaction = transactionRetrievalUseCase.getTransactionById(id); + TransactionResponse response = TransactionResponse.fromDomain(transaction); + return new ResponseEntity<>(response, HttpStatus.OK); + } +} \ No newline at end of file diff --git a/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/dto/TransactionRequest.java b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/dto/TransactionRequest.java new file mode 100644 index 0000000000..e643a7acfa --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/dto/TransactionRequest.java @@ -0,0 +1,10 @@ +package com.jotace.transaction.infrastructure.adapter.in.rest.dto; + +// Public Record for the request body. Used by the REST Controller. +public record TransactionRequest( + String accountExternalIdDebit, + String accountExternalIdCredit, + int tranferTypeId, + double value +) { +} \ No newline at end of file diff --git a/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/dto/TransactionResponse.java b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/dto/TransactionResponse.java new file mode 100644 index 0000000000..9290d48670 --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/in/rest/dto/TransactionResponse.java @@ -0,0 +1,32 @@ +package com.jotace.transaction.infrastructure.adapter.in.rest.dto; + +import com.jotace.transaction.domain.model.Transaction; + +import java.time.Instant; +import java.util.UUID; + +// Public Record for the standardized response format. +public record TransactionResponse( + UUID transactionExternalId, + TransactionType transactionType, + TransactionStatus transactionStatus, + double value, + Instant createdAt +) { + // Inner Record representing the nested transactionType object + public record TransactionType(int name) {} + + // Inner Record representing the nested transactionStatus object + public record TransactionStatus(String name) {} + + // + public static TransactionResponse fromDomain(Transaction transaction) { + return new TransactionResponse( + transaction.transactionExternalId(), + new TransactionType(transaction.tranferTypeId()), + new TransactionStatus(transaction.transactionStatus()), + transaction.value(), + transaction.createdAt() + ); + } +} diff --git a/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/JpaTransactionAdapter.java b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/JpaTransactionAdapter.java new file mode 100644 index 0000000000..b03287cede --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/JpaTransactionAdapter.java @@ -0,0 +1,71 @@ +package com.jotace.transaction.infrastructure.adapter.out; + +import com.jotace.transaction.application.port.out.TransactionRepositoryPort; +import com.jotace.transaction.domain.model.Transaction; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.UUID; + +@Component +public class JpaTransactionAdapter implements TransactionRepositoryPort { + + private final TransactionRepository jpaRepository; + + public JpaTransactionAdapter(TransactionRepository jpaRepository) { + this.jpaRepository = jpaRepository; + } + + // Domain to Infrastructure + private TransactionEntity toEntity(Transaction domain) { + return new TransactionEntity( + domain.transactionExternalId(), + domain.accountExternalIdDebit(), + domain.accountExternalIdCredit(), + domain.tranferTypeId(), + domain.value(), + domain.transactionStatus(), + domain.createdAt() + ); + } + + // Infrastructure to Domain + private Transaction toDomain(TransactionEntity entity) { + return new Transaction( + entity.getTransactionExternalId(), + entity.getAccountExternalIdDebit(), + entity.getAccountExternalIdCredit(), + entity.getTransferTypeId(), + entity.getValue(), + entity.getTransactionStatus(), + entity.getCreatedAt() + ); + } + + @Override + public Transaction save(Transaction transaction) { + TransactionEntity entity = toEntity(transaction); + TransactionEntity savedEntity = jpaRepository.save(entity); + return toDomain(savedEntity); + } + + @Override + public Optional findByExternalId(UUID transactionExternalId) { + return jpaRepository.findByTransactionExternalId(transactionExternalId) + .map(this::toDomain); + } + + @Override + public void updateStatus(UUID transactionExternalId, String newStatus) { + TransactionEntity entity = jpaRepository.findByTransactionExternalId( + transactionExternalId + ).orElseThrow(() -> new IllegalArgumentException("Transaction not found")); + + entity.setTransactionStatus(newStatus); + + TransactionEntity saved = jpaRepository.save(entity); + + toDomain(saved); + } + +} diff --git a/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/KafkaEventAdapter.java b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/KafkaEventAdapter.java new file mode 100644 index 0000000000..da2afee1de --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/KafkaEventAdapter.java @@ -0,0 +1,37 @@ +package com.jotace.transaction.infrastructure.adapter.out; + +import com.jotace.transaction.application.port.out.EventProducerPort; +import com.jotace.transaction.domain.model.Transaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Component +public class KafkaEventAdapter implements EventProducerPort { + + private static final Logger log = LoggerFactory.getLogger(KafkaEventAdapter.class); + private static final String TOPIC = "transaction-created"; + + private final KafkaTemplate kafkaTemplate; + + public KafkaEventAdapter(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Override + public void sendTransactionCreatedEvent(Transaction transaction) { + try { + kafkaTemplate.send(TOPIC, transaction.transactionExternalId().toString(), transaction); + log.info("Event published to Kafka TOPIC: {} | Key: {} | Status: {}", + TOPIC, + transaction.transactionExternalId(), + transaction.transactionStatus()); + } catch (Exception e) { + log.error("Failed to send transaction event to Kafka for ID: {}", + transaction.transactionExternalId(), e); + throw new IllegalStateException("Kafka publishing failed for transaction.", e); + } + } + +} \ No newline at end of file diff --git a/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/TransactionEntity.java b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/TransactionEntity.java new file mode 100644 index 0000000000..d44f43dda9 --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/TransactionEntity.java @@ -0,0 +1,61 @@ +package com.jotace.transaction.infrastructure.adapter.out; + +import jakarta.persistence.*; +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "transaction") +public class TransactionEntity { + + @Id + @Column(name = "transaction_external_id", unique = true, nullable = false) + private UUID transactionExternalId; + + @Column(name = "account_external_id_debit", nullable = false) + private String accountExternalIdDebit; + + @Column(name = "account_external_id_credit", nullable = false) + private String accountExternalIdCredit; + + @Column(name = "transfer_type_id", nullable = false) + private int tranferTypeId; + + @Column(name = "value", nullable = false) + private double value; + + @Column(name = "transaction_status", nullable = false) + private String transactionStatus; + + @Column(name = "created_at", nullable = false) + private Instant createdAt; + + public TransactionEntity() { + } + + // Constructor used by the adapter to save a new record + public TransactionEntity(UUID transactionExternalId, String accountExternalIdDebit, String accountExternalIdCredit, int tranferTypeId, double value, String transactionStatus, Instant createdAt) { + this.transactionExternalId = transactionExternalId; + this.accountExternalIdDebit = accountExternalIdDebit; + this.accountExternalIdCredit = accountExternalIdCredit; + this.tranferTypeId = tranferTypeId; + this.value = value; + this.transactionStatus = transactionStatus; + this.createdAt = createdAt; + } + + public UUID getTransactionExternalId() { return transactionExternalId; } + public void setTransactionExternalId(UUID transactionExternalId) { this.transactionExternalId = transactionExternalId; } + public String getAccountExternalIdDebit() { return accountExternalIdDebit; } + public void setAccountExternalIdDebit(String accountExternalIdDebit) { this.accountExternalIdDebit = accountExternalIdDebit; } + public String getAccountExternalIdCredit() { return accountExternalIdCredit; } + public void setAccountExternalIdCredit(String accountExternalIdCredit) { this.accountExternalIdCredit = accountExternalIdCredit; } + public int getTransferTypeId() { return tranferTypeId; } + public void setTransferTypeId(int tranferTypeId) { this.tranferTypeId = tranferTypeId; } + public double getValue() { return value; } + public void setValue(double value) { this.value = value; } + public String getTransactionStatus() { return transactionStatus; } + public void setTransactionStatus(String transactionStatus) { this.transactionStatus = transactionStatus; } + public Instant getCreatedAt() { return createdAt; } + public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } +} diff --git a/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/TransactionRepository.java b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/TransactionRepository.java new file mode 100644 index 0000000000..fb7445d3fe --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/adapter/out/TransactionRepository.java @@ -0,0 +1,14 @@ +package com.jotace.transaction.infrastructure.adapter.out; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface TransactionRepository extends JpaRepository { + + // Custom query method to find the entity by its public UUID + Optional findByTransactionExternalId(UUID transactionExternalId); +} diff --git a/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/config/ApplicationConfig.java b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/config/ApplicationConfig.java new file mode 100644 index 0000000000..6243893782 --- /dev/null +++ b/ms-transaction/src/main/java/com/jotace/transaction/infrastructure/config/ApplicationConfig.java @@ -0,0 +1,20 @@ +package com.jotace.transaction.infrastructure.config; + +import com.jotace.transaction.application.port.out.EventProducerPort; +import com.jotace.transaction.application.port.out.TransactionRepositoryPort; +import com.jotace.transaction.application.service.TransactionService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ApplicationConfig { + + @Bean + public TransactionService transactionService( + TransactionRepositoryPort transactionRepositoryPort, + EventProducerPort eventProducer) { + + return new TransactionService(transactionRepositoryPort, eventProducer); + + } +} diff --git a/ms-transaction/src/main/resources/application.yaml b/ms-transaction/src/main/resources/application.yaml new file mode 100644 index 0000000000..08f59fb6a6 --- /dev/null +++ b/ms-transaction/src/main/resources/application.yaml @@ -0,0 +1,43 @@ +spring: + application: + name: ms-transaction + + datasource: + url: jdbc:postgresql://localhost:5432/transaction_db + username: postgres + password: postgres + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 20 + minimum-idle: 5 + idle-timeout: 30000 + + jpa: + show-sql: false + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: true + hibernate: + ddl-auto: update + open-in-view: false + + kafka: + bootstrap-servers: localhost:9092 + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + properties: + spring.json.add.type.headers: false + acks: all + retries: 10 + consumer: + group-id: transaction-status-group + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + spring.json.value.default.type: com.jotace.transaction.infrastructure.adapter.in.consumer.TransactionStatusUpdatedEvent + +server: + port: 8081 \ No newline at end of file diff --git a/ms-transaction/src/test/java/com/jotace/transaction/MsTransactionApplicationTests.java b/ms-transaction/src/test/java/com/jotace/transaction/MsTransactionApplicationTests.java new file mode 100644 index 0000000000..abf38945d5 --- /dev/null +++ b/ms-transaction/src/test/java/com/jotace/transaction/MsTransactionApplicationTests.java @@ -0,0 +1,13 @@ +package com.jotace.transaction; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class MsTransactionApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/ms-transaction/src/test/java/com/jotace/transaction/TransactionServiceTest.java b/ms-transaction/src/test/java/com/jotace/transaction/TransactionServiceTest.java new file mode 100644 index 0000000000..6a21ca9b5b --- /dev/null +++ b/ms-transaction/src/test/java/com/jotace/transaction/TransactionServiceTest.java @@ -0,0 +1,123 @@ +package com.jotace.transaction; + +import com.jotace.transaction.application.port.out.EventProducerPort; +import com.jotace.transaction.application.port.out.TransactionRepositoryPort; +import com.jotace.transaction.application.service.TransactionService; +import com.jotace.transaction.domain.model.Transaction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +// Uses MockitoExtension to enable annotation processing for Mockito mocks +@ExtendWith(MockitoExtension.class) +public class TransactionServiceTest { + + // Mock the external dependencies (Outgoing Ports) + @Mock + private TransactionRepositoryPort transactionRepositoryPort; + + @Mock + private EventProducerPort eventProducer; + + // Inject the mocks into the service class we want to test + @InjectMocks + private TransactionService transactionService; + + // Fixed dummy data for testing + private static final String ACCOUNT_DEBIT = "acc-debit-123"; + private static final String ACCOUNT_CREDIT = "acc-credit-456"; + private static final int TRANSFER_TYPE_ID = 1; + private static final double VALUE = 100.00; + private static final UUID EXTERNAL_ID = UUID.randomUUID(); + private static final Instant CREATED_AT = Instant.now(); + private static final String STATUS_PENDING = "pending"; + + // Setup method to initialize the service, although @InjectMocks handles it + @BeforeEach + void setUp() { + // Optional: Can be used for shared setup logic if needed + } + + @Test + void createTransaction_shouldPersistAndPublishEvent() { + // Arrange + // 1. Prepare the transaction object that the repository will return after persistence. + // It must include the generated UUID and creation timestamp (which the DB/Adapter normally provides). + Transaction transactionAfterSave = new Transaction( + EXTERNAL_ID, + ACCOUNT_DEBIT, + ACCOUNT_CREDIT, + TRANSFER_TYPE_ID, + VALUE, + STATUS_PENDING, + CREATED_AT + ); + + // ArgumentCaptor to capture the Transaction object passed to the save method + ArgumentCaptor captor = ArgumentCaptor.forClass(Transaction.class); + + // Define Mock Behavior: + // When the repository's save method is called with ANY Transaction object, + // return the prepared 'transactionAfterSave'. + when(transactionRepositoryPort.save(any(Transaction.class))).thenReturn(transactionAfterSave); + + // Act + Transaction result = transactionService.createTransaction( + ACCOUNT_DEBIT, + ACCOUNT_CREDIT, + TRANSFER_TYPE_ID, + VALUE + ); + + // Assert + + // 1. Verify the persistence interaction + // Verify that the save method was called exactly once and capture the argument passed + verify(transactionRepositoryPort, times(1)).save(captor.capture()); + + // Assert on the initial state of the transaction before saving: + Transaction transactionBeforeSave = captor.getValue(); + assertEquals(ACCOUNT_DEBIT, transactionBeforeSave.accountExternalIdDebit()); + assertEquals(ACCOUNT_CREDIT, transactionBeforeSave.accountExternalIdCredit()); + assertEquals(STATUS_PENDING, transactionBeforeSave.transactionStatus()); + // Verify that the external ID and creation time are null/default (as they are generated later) + assertNotNull(transactionBeforeSave.transactionExternalId()); + assertNotNull(transactionBeforeSave.createdAt()); + + // 2. Verify the event publishing interaction + // Verify that the event producer was called exactly once with the *saved* transaction + verify(eventProducer, times(1)).sendTransactionCreatedEvent(transactionAfterSave); + + // 3. Verify the final result + // Check if the returned transaction matches the one provided by the mock repository + assertNotNull(result); + assertEquals(EXTERNAL_ID, result.transactionExternalId()); + assertEquals(STATUS_PENDING, result.transactionStatus()); + } + + @Test + void createTransaction_shouldThrowException_whenRepositoryFails() { + // Arrange + // Define Mock Behavior: Throw an exception when the repository's save method is called + when(transactionRepositoryPort.save(any(Transaction.class))).thenThrow(new RuntimeException("Database error")); + + // Act & Assert + // Verify that calling the service method throws the expected exception + assertThrows(RuntimeException.class, () -> + transactionService.createTransaction(ACCOUNT_DEBIT, ACCOUNT_CREDIT, TRANSFER_TYPE_ID, VALUE) + ); + + // Ensure that if persistence fails, no event is published + verify(eventProducer, never()).sendTransactionCreatedEvent(any(Transaction.class)); + } +}