diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000000..7bd5ebfaf1
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "associatedIndex": 4
+}
+
+
+
+
+
+ {
+ "keyToString": {
+ "ModuleVcsDetector.initialDetectionPerformed": "true",
+ "RunOnceActivity.MCP Project settings loaded": "true",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "RunOnceActivity.typescript.service.memoryLimit.init": "true",
+ "git-widget-placeholder": "anti-gravity",
+ "ignore.virus.scanning.warn.message": "true",
+ "kotlin-language-version-configured": "true",
+ "last_opened_file_path": "C:/proyectos/back/app-nodejs-codechallenge",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "settings.editor.selected.configurable": "preferences.general",
+ "vue.rearranger.settings.migration": "true"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+ 1772246291130
+
+
+ 1772246291130
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CHALLENGE.md b/CHALLENGE.md
new file mode 100644
index 0000000000..b067a71026
--- /dev/null
+++ b/CHALLENGE.md
@@ -0,0 +1,82 @@
+# Yape Code Challenge :rocket:
+
+Our code challenge will let you marvel us with your Jedi coding skills :smile:.
+
+Don't forget that the proper way to submit your work is to fork the repo and create a PR :wink: ... have fun !!
+
+- [Problem](#problem)
+- [Tech Stack](#tech_stack)
+- [Send us your challenge](#send_us_your_challenge)
+
+# Problem
+
+Every time a financial transaction is created it must be validated by our anti-fraud microservice and then the same service sends a message back to update the transaction status.
+For now, we have only three transaction statuses:
+
+
+ pending
+ approved
+ rejected
+
+
+Every transaction with a value greater than 1000 should be rejected.
+
+```mermaid
+ flowchart LR
+ Transaction -- Save Transaction with pending Status --> transactionDatabase[(Database)]
+ Transaction --Send transaction Created event--> Anti-Fraud
+ Anti-Fraud -- Send transaction Status Approved event--> Transaction
+ Anti-Fraud -- Send transaction Status Rejected event--> Transaction
+ Transaction -- Update transaction Status event--> transactionDatabase[(Database)]
+```
+
+# Tech Stack
+
+
+ Node. You can use any framework you want (i.e. Nestjs with an ORM like TypeOrm or Prisma)
+ Any database
+ Kafka
+
+
+We do provide a `Dockerfile` to help you get started with a dev environment.
+
+You must have two resources:
+
+1. Resource to create a transaction that must containt:
+
+```json
+{
+ "accountExternalIdDebit": "Guid",
+ "accountExternalIdCredit": "Guid",
+ "tranferTypeId": 1,
+ "value": 120
+}
+```
+
+2. Resource to retrieve a transaction
+
+```json
+{
+ "transactionExternalId": "Guid",
+ "transactionType": {
+ "name": ""
+ },
+ "transactionStatus": {
+ "name": ""
+ },
+ "value": 120,
+ "createdAt": "Date"
+}
+```
+
+## Optional
+
+You can use any approach to store transaction data but you should consider that we may deal with high volume scenarios where we have a huge amount of writes and reads for the same data at the same time. How would you tackle this requirement?
+
+You can use Graphql;
+
+# Send us your challenge
+
+When you finish your challenge, after forking a repository, you **must** open a pull request to our repository. There are no limitations to the implementation, you can follow the programming paradigm, modularization, and style that you feel is the most appropriate solution.
+
+If you have any questions, please let us know.
diff --git a/README.md b/README.md
index b067a71026..ab7671b2d6 100644
--- a/README.md
+++ b/README.md
@@ -1,82 +1,224 @@
-# Yape Code Challenge :rocket:
+# Yape Code Challenge Solution 🚀
-Our code challenge will let you marvel us with your Jedi coding skills :smile:.
+Este repositorio contiene la solución para el Desafío de Código Yape. El problema consiste en validar transacciones financieras mediante un servicio antifraude, garantizando que cada transacción mayor a 1000 sea rechazada y actualizando el estado de la transacción según corresponda.
-Don't forget that the proper way to submit your work is to fork the repo and create a PR :wink: ... have fun !!
+> **Nota:** Las instrucciones originales del desafío se encuentran en [CHALLENGE.md](CHALLENGE.md).
-- [Problem](#problem)
-- [Tech Stack](#tech_stack)
-- [Send us your challenge](#send_us_your_challenge)
+## 🏗 Architecture & Tech Stack
-# Problem
+Para manejar **Escenarios de Alto Volumen** de manera efectiva, se ha implementado una **Arquitectura Orientada a Eventos** combinada con **Programación Reactiva**.
-Every time a financial transaction is created it must be validated by our anti-fraud microservice and then the same service sends a message back to update the transaction status.
-For now, we have only three transaction statuses:
+### Technologies Used
-
- pending
- approved
- rejected
-
+- **Java 21**: Aprovechando las características modernas del lenguaje para un mejor rendimiento.
+- **Spring Boot 4.0.3 (WebFlux)**: Enfoque reactivo y no bloqueante para manejar miles de solicitudes concurrentes con un consumo mínimo de recursos.
+- **Apache Kafka**: Utilizado como broker de mensajes para la comunicación asíncrona entre microservicios, asegurando alto rendimiento, resiliencia y procesamiento desacoplado.
+- **PostgreSQL (R2DBC)**: Base de datos relacional robusta conectada a través de un driver reactivo para prevenir bloqueos de I/O.
+- **Redis**: Base de datos en memoria utilizada como caché para mejorar el rendimiento.
+- **GraphQL**: API para la recuperación de datos.
+- **Gradle**: Herramienta de automatización de compilación.
+- **Docker & Docker Compose**: Contenedorización de la infraestructura y microservicios para una implementación y pruebas sin interrupciones.
-Every transaction with a value greater than 1000 should be rejected.
+### Microservices
-```mermaid
- flowchart LR
- Transaction -- Save Transaction with pending Status --> transactionDatabase[(Database)]
- Transaction --Send transaction Created event--> Anti-Fraud
- Anti-Fraud -- Send transaction Status Approved event--> Transaction
- Anti-Fraud -- Send transaction Status Rejected event--> Transaction
- Transaction -- Update transaction Status event--> transactionDatabase[(Database)]
+1. **Transaction Service (`transaction-service`)**:
+ - Expone endpoints reactivos para crear y recuperar transacciones.
+ - Gestiona la base de datos PostgreSQL y persiste las transacciones inicialmente como `PENDING`.
+ - Utiliza Redis como caché para mejorar el rendimiento.
+ - Expone una API GraphQL para la recuperación de datos.
+ - Produce eventos asíncronos (`transaction-created`) a Kafka.
+ - Escucha las actualizaciones de estado (`transaction-status`) desde Kafka para actualizar la base de datos de forma asíncrona.
+ - Implementado utilizando patrones de **Arquitectura Limpia** (capas de Dominio, Aplicación e Infraestructura).
+
+2. **Anti-Fraud Service (`anti-fraud-service`)**:
+ - Actúa puramente como un consumidor de eventos robusto en segundo plano.
+ - Valida las transacciones estrictamente según la regla de negocio `value > 1000`.
+ - Responde publicando el estado final (`APPROVED` o `REJECTED`) de vuelta al tópico de Kafka `transaction-status`.
+
+## ⚙️ How to Run Locally
+
+### Prerequisites
+- [Docker](https://www.docker.com/) and [Docker Compose](https://docs.docker.com/compose/) installed.
+- (Optional) Java 21 to run outside of containers.
+
+### 1. Build and Run via Docker Compose (Recommended)
+
+La forma más sencilla de iniciar todo el ecosistema (Postgres, Zookeeper, Kafka, Transaction Service, Anti-Fraud Service) es utilizando Docker Compose.
+
+```bash
+# Desde la raíz del proyecto, construye e inicia todos los contenedores en segundo plano
+docker-compose up --build -d
```
-# Tech Stack
+Puedes verificar que los 5 contenedores estén funcionando correctamente:
+```bash
+docker-compose ps
+```
+
+Para detener los servicios más tarde:
+```bash
+docker-compose down
+```
+
+### 2. Manual Run using Gradle (Development mode)
+Si prefieres ejecutar los microservicios localmente fuera de Docker (teniendo solo la infraestructura en Docker):
+
+```bash
+# 1. Inicia solo la infraestructura (Postgres, ZooKeeper, Kafka)
+docker-compose up -d postgres zookeeper kafka
+
+# 2. Ejecuta el Transaction Service
+cd transaction-service
+./gradlew bootRun
-
- Node. You can use any framework you want (i.e. Nestjs with an ORM like TypeOrm or Prisma)
- Any database
- Kafka
-
+# 3. Ejecuta el Anti-Fraud Service (en una terminal separada)
+cd anti-fraud-service
+./gradlew bootRun
+```
+
+## 🛠 API Usage
-We do provide a `Dockerfile` to help you get started with a dev environment.
+Una vez que el ecosistema esté funcionando, la **API del Transaction Service** estará disponible en `http://localhost:8080`.
-You must have two resources:
+### 1. Create a Transaction
+Crea una nueva transacción. Esto la almacenará instantáneamente como `PENDING` e iniciará el flujo de validación de Kafka.
-1. Resource to create a transaction that must containt:
+**Endpoint:** `POST /api/transactions`
+**Body:**
```json
{
- "accountExternalIdDebit": "Guid",
- "accountExternalIdCredit": "Guid",
+ "accountExternalIdDebit": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "accountExternalIdCredit": "8f285f64-5717-4562-b3fc-2c963f66afa7",
"tranferTypeId": 1,
- "value": 120
+ "value": 1200.50
}
```
-2. Resource to retrieve a transaction
-
+**Response:** (200 OK)
```json
{
- "transactionExternalId": "Guid",
+ "transactionExternalId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
"transactionType": {
- "name": ""
+ "name": "1"
},
"transactionStatus": {
- "name": ""
+ "name": "PENDING"
},
- "value": 120,
- "createdAt": "Date"
+ "value": 1200.5,
+ "createdAt": "2026-03-01T00:00:00.00000"
}
```
+*(Notice: Since `1200.5 > 1000`, the Anti-Fraud Service will immediately pick up the event backing this response and update the status to `REJECTED` in the database).*
-## Optional
+### 2. Retrieve a Transaction
+Consulta el estado final de tu transacción. Para cuando la consultes, el flujo asíncrono de Kafka ya debería haber procesado el evento.
-You can use any approach to store transaction data but you should consider that we may deal with high volume scenarios where we have a huge amount of writes and reads for the same data at the same time. How would you tackle this requirement?
+**Endpoint:** `GET /api/transactions/{transactionExternalId}`
-You can use Graphql;
+**Response:** (200 OK)
+```json
+{
+ "transactionExternalId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
+ "transactionType": {
+ "name": "1"
+ },
+ "transactionStatus": {
+ "name": "REJECTED"
+ },
+ "value": 1200.5,
+ "createdAt": "2026-03-01T00:00:00.00000"
+}
+```
+## GraphQL
+Si prefieres realizar consultas mediante GraphQL, utiliza el endpoint `http://localhost:8080/graphiql`.
+
+**Query:**
+```graphql
+query GetTransaction($id: String!) {
+ getTransaction(transactionExternalId: $id) {
+ transactionExternalId
+ transactionType {
+ name
+ }
+ transactionStatus {
+ name
+ }
+ value
+ createdAt
+ }
+}
+```
-# Send us your challenge
+**Variables:**
+```json
+{
+ "id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
+}
+```
-When you finish your challenge, after forking a repository, you **must** open a pull request to our repository. There are no limitations to the implementation, you can follow the programming paradigm, modularization, and style that you feel is the most appropriate solution.
+**Response:**
+```json
+{
+ "data": {
+ "getTransaction": {
+ "transactionExternalId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
+ "transactionType": { "name": "1" },
+ "transactionStatus": { "name": "REJECTED" },
+ "value": 1200.5,
+ "createdAt": "2026-03-01T00:00:00.00000"
+ }
+ }
+}
+```
+```
+**Mutations:**
+```graphql
+mutation CreateTransaction($input: CreateTransactionInput!) {
+ createTransaction(input: $input) {
+ transactionExternalId
+ transactionType {
+ name
+ }
+ transactionStatus {
+ name
+ }
+ value
+ createdAt
+ }
+}
+```
+
+**Variables:**
+```json
+{
+ "input": {
+ "accountExternalIdDebit": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "accountExternalIdCredit": "8f285f64-5717-4562-b3fc-2c963f66afa7",
+ "tranferTypeId": 1,
+ "value": 1200.50
+ }
+}
+```
+
+**Response:**
+```json
+{
+ "data": {
+ "createTransaction": {
+ "transactionExternalId": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d",
+ "transactionType": { "name": "1" },
+ "transactionStatus": { "name": "PENDING" },
+ "value": 1200.5,
+ "createdAt": "2026-03-01T00:00:00.00000"
+ }
+ }
+}
+```
+```
-If you have any questions, please let us know.
+## 💡 About High Volume Scenarios
+La arquitectura **Event-Driven + WebFlux** adoptada resuelve de forma nativa el desafío de los "Escenarios de Alto Volumen":
+- **Resiliencia en Escritura (Write-Heavy)**: La solicitud HTTP no espera la validación antifraude. Simplemente envía el evento a través de R2DBC+Kafka y retorna exitosamente. Kafka actúa como un buffer masivo, absorbiendo miles de eventos concurrentes sin bloquear la aplicación.
+- **Segregación Lectura/Escritura (Fundamentos CQRS)**: Actualmente, tanto la lectura como la escritura aprovechan intensamente la E/S no bloqueante. Para escenarios de lectura extrema, una capa de caché (como Redis) y una API GraphQL podrían integrarse fácilmente sobre esta arquitectura.
+- **Escalabilidad Horizontal**: Cada microservicio puede instanciarse/escalarse múltiples veces en un clúster de Kubernetes; los Grupos de Consumidores de Kafka manejan la distribución de carga de forma nativa.
diff --git a/anti-fraud-service/.gitattributes b/anti-fraud-service/.gitattributes
new file mode 100644
index 0000000000..8af972cded
--- /dev/null
+++ b/anti-fraud-service/.gitattributes
@@ -0,0 +1,3 @@
+/gradlew text eol=lf
+*.bat text eol=crlf
+*.jar binary
diff --git a/anti-fraud-service/.gitignore b/anti-fraud-service/.gitignore
new file mode 100644
index 0000000000..c2065bc262
--- /dev/null
+++ b/anti-fraud-service/.gitignore
@@ -0,0 +1,37 @@
+HELP.md
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
diff --git a/anti-fraud-service/Dockerfile b/anti-fraud-service/Dockerfile
new file mode 100644
index 0000000000..e4637c21b7
--- /dev/null
+++ b/anti-fraud-service/Dockerfile
@@ -0,0 +1,16 @@
+# Stage 1: Build
+FROM gradle:8-jdk21 AS build
+WORKDIR /app
+COPY build.gradle settings.gradle ./
+COPY src ./src
+COPY gradle ./gradle
+COPY gradlew ./
+COPY gradlew.bat ./
+RUN chmod +x gradlew
+RUN ./gradlew build -x test
+
+# Stage 2: Run
+FROM eclipse-temurin:21-jre
+WORKDIR /app
+COPY --from=build /app/build/libs/*.jar app.jar
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/anti-fraud-service/build.gradle b/anti-fraud-service/build.gradle
new file mode 100644
index 0000000000..2855901a45
--- /dev/null
+++ b/anti-fraud-service/build.gradle
@@ -0,0 +1,39 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot' version '4.0.3'
+ id 'io.spring.dependency-management' version '1.1.7'
+}
+
+group = 'com.yape.challenge'
+version = '0.0.1-SNAPSHOT'
+description = 'anti fraud service'
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
+}
+
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-kafka'
+ implementation 'org.springframework.boot:spring-boot-starter-webflux'
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
+ testImplementation 'org.springframework.boot:spring-boot-starter-kafka-test'
+ testImplementation 'org.springframework.boot:spring-boot-starter-webflux-test'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
diff --git a/anti-fraud-service/gradle/wrapper/gradle-wrapper.jar b/anti-fraud-service/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..61285a659d
Binary files /dev/null and b/anti-fraud-service/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/anti-fraud-service/gradle/wrapper/gradle-wrapper.properties b/anti-fraud-service/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..37f78a6af8
--- /dev/null
+++ b/anti-fraud-service/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/anti-fraud-service/gradlew b/anti-fraud-service/gradlew
new file mode 100644
index 0000000000..adff685a03
--- /dev/null
+++ b/anti-fraud-service/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+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
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/anti-fraud-service/gradlew.bat b/anti-fraud-service/gradlew.bat
new file mode 100644
index 0000000000..c4bdd3ab8e
--- /dev/null
+++ b/anti-fraud-service/gradlew.bat
@@ -0,0 +1,93 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/anti-fraud-service/settings.gradle b/anti-fraud-service/settings.gradle
new file mode 100644
index 0000000000..c19acbf68d
--- /dev/null
+++ b/anti-fraud-service/settings.gradle
@@ -0,0 +1,4 @@
+plugins {
+ id 'org.gradle.toolchains.foojay-resolver-convention' version '1.0.0'
+}
+rootProject.name = 'anti-fraud-service'
diff --git a/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/AntiFraudServiceApplication.java b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/AntiFraudServiceApplication.java
new file mode 100644
index 0000000000..d0ce7e4cbf
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/AntiFraudServiceApplication.java
@@ -0,0 +1,13 @@
+package com.yape.challenge.antifraud;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class AntiFraudServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(AntiFraudServiceApplication.class, args);
+ }
+
+}
diff --git a/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/application/usecase/ValidateTransactionUseCase.java b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/application/usecase/ValidateTransactionUseCase.java
new file mode 100644
index 0000000000..4ae5ddee72
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/application/usecase/ValidateTransactionUseCase.java
@@ -0,0 +1,29 @@
+package com.yape.challenge.antifraud.application.usecase;
+
+import com.yape.challenge.antifraud.domain.messaging.EventPublisher;
+import com.yape.challenge.antifraud.domain.model.Transaction;
+import com.yape.challenge.antifraud.domain.model.TransactionStatus;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ValidateTransactionUseCase {
+
+ private final EventPublisher eventPublisher;
+
+ public Mono execute(Transaction transaction) {
+ log.info("Validating transaction {} with value {}", transaction.getId(), transaction.getValue());
+
+ TransactionStatus status = transaction.getValue() > 1000
+ ? TransactionStatus.REJECTED
+ : TransactionStatus.APPROVED;
+
+ return eventPublisher.publishTransactionStatusUpdate(transaction.getId(), status)
+ .doOnSuccess(v -> log.info("Successfully validated transaction {} with status {}", transaction.getId(),
+ status));
+ }
+}
diff --git a/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/domain/messaging/EventPublisher.java b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/domain/messaging/EventPublisher.java
new file mode 100644
index 0000000000..b9d696444a
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/domain/messaging/EventPublisher.java
@@ -0,0 +1,10 @@
+package com.yape.challenge.antifraud.domain.messaging;
+
+import com.yape.challenge.antifraud.domain.model.TransactionStatus;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+public interface EventPublisher {
+ Mono publishTransactionStatusUpdate(UUID transactionId, TransactionStatus status);
+}
diff --git a/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/domain/model/Transaction.java b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/domain/model/Transaction.java
new file mode 100644
index 0000000000..6df5a9680c
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/domain/model/Transaction.java
@@ -0,0 +1,17 @@
+package com.yape.challenge.antifraud.domain.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Transaction {
+ private UUID id;
+ private Double value;
+}
diff --git a/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/domain/model/TransactionStatus.java b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/domain/model/TransactionStatus.java
new file mode 100644
index 0000000000..322d2f4af5
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/domain/model/TransactionStatus.java
@@ -0,0 +1,7 @@
+package com.yape.challenge.antifraud.domain.model;
+
+public enum TransactionStatus {
+ PENDING,
+ APPROVED,
+ REJECTED
+}
diff --git a/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/in/kafka/TransactionCreatedListener.java b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/in/kafka/TransactionCreatedListener.java
new file mode 100644
index 0000000000..6c70891640
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/in/kafka/TransactionCreatedListener.java
@@ -0,0 +1,38 @@
+package com.yape.challenge.antifraud.infrastructure.adapters.in.kafka;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectMapper;
+import com.yape.challenge.antifraud.application.usecase.ValidateTransactionUseCase;
+import com.yape.challenge.antifraud.domain.model.Transaction;
+import com.yape.challenge.antifraud.infrastructure.adapters.in.kafka.dto.TransactionCreatedEvent;
+import com.yape.challenge.antifraud.infrastructure.config.KafkaTopicConfig;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TransactionCreatedListener {
+
+ private final ValidateTransactionUseCase validateTransactionUseCase;
+ private final ObjectMapper objectMapper;
+
+ @KafkaListener(topics = KafkaTopicConfig.TRANSACTION_CREATED_TOPIC, groupId = "anti-fraud-group")
+ public void consumeTransactionCreatedEvent(String message) {
+ log.info("Received transaction created event: {}", message);
+
+ TransactionCreatedEvent event = objectMapper.readValue(message, TransactionCreatedEvent.class);
+
+ Transaction transaction = Transaction.builder()
+ .id(event.getTransactionId())
+ .value(event.getValue())
+ .build();
+
+ validateTransactionUseCase.execute(transaction)
+ .doOnError(e -> log.error("Failed to process transaction validation", e))
+ .subscribe();
+
+ }
+}
diff --git a/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/in/kafka/dto/TransactionCreatedEvent.java b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/in/kafka/dto/TransactionCreatedEvent.java
new file mode 100644
index 0000000000..c7bc3a44f9
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/in/kafka/dto/TransactionCreatedEvent.java
@@ -0,0 +1,11 @@
+package com.yape.challenge.antifraud.infrastructure.adapters.in.kafka.dto;
+
+import lombok.Data;
+
+import java.util.UUID;
+
+@Data
+public class TransactionCreatedEvent {
+ private UUID transactionId;
+ private Double value;
+}
diff --git a/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/out/kafka/KafkaEventPublisherAdapter.java b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/out/kafka/KafkaEventPublisherAdapter.java
new file mode 100644
index 0000000000..99a6e23136
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/out/kafka/KafkaEventPublisherAdapter.java
@@ -0,0 +1,43 @@
+package com.yape.challenge.antifraud.infrastructure.adapters.out.kafka;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectMapper;
+import com.yape.challenge.antifraud.domain.messaging.EventPublisher;
+import com.yape.challenge.antifraud.domain.model.TransactionStatus;
+import com.yape.challenge.antifraud.infrastructure.adapters.out.kafka.dto.TransactionStatusUpdateEvent;
+import com.yape.challenge.antifraud.infrastructure.config.KafkaTopicConfig;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class KafkaEventPublisherAdapter implements EventPublisher {
+
+ private final KafkaTemplate kafkaTemplate;
+ private final ObjectMapper objectMapper;
+
+ @Override
+ public Mono publishTransactionStatusUpdate(UUID transactionId, TransactionStatus status) {
+ return Mono.fromRunnable(() -> {
+ try {
+ TransactionStatusUpdateEvent event = TransactionStatusUpdateEvent.builder()
+ .transactionId(transactionId)
+ .status(status)
+ .build();
+
+ String payload = objectMapper.writeValueAsString(event);
+ kafkaTemplate.send(KafkaTopicConfig.TRANSACTION_STATUS_TOPIC, transactionId.toString(), payload);
+ log.info("Published TransactionStatusUpdateEvent for ID: {} with status: {}", transactionId, status);
+
+ } catch (JacksonException e) {
+ log.error("Error serializing TransactionStatusUpdateEvent", e);
+ }
+ });
+ }
+}
diff --git a/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/out/kafka/dto/TransactionStatusUpdateEvent.java b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/out/kafka/dto/TransactionStatusUpdateEvent.java
new file mode 100644
index 0000000000..fed1a07c95
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/adapters/out/kafka/dto/TransactionStatusUpdateEvent.java
@@ -0,0 +1,14 @@
+package com.yape.challenge.antifraud.infrastructure.adapters.out.kafka.dto;
+
+import com.yape.challenge.antifraud.domain.model.TransactionStatus;
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.UUID;
+
+@Data
+@Builder
+public class TransactionStatusUpdateEvent {
+ private UUID transactionId;
+ private TransactionStatus status;
+}
diff --git a/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/config/KafkaTopicConfig.java b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/config/KafkaTopicConfig.java
new file mode 100644
index 0000000000..1f4c4409c0
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/challenge/antifraud/infrastructure/config/KafkaTopicConfig.java
@@ -0,0 +1,29 @@
+package com.yape.challenge.antifraud.infrastructure.config;
+
+import org.apache.kafka.clients.admin.NewTopic;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.config.TopicBuilder;
+
+@Configuration
+public class KafkaTopicConfig {
+
+ public static final String TRANSACTION_CREATED_TOPIC = "transaction-created";
+ public static final String TRANSACTION_STATUS_TOPIC = "transaction-status";
+
+ @Bean
+ public NewTopic transactionCreatedTopic() {
+ return TopicBuilder.name(TRANSACTION_CREATED_TOPIC)
+ .partitions(3)
+ .replicas(1)
+ .build();
+ }
+
+ @Bean
+ public NewTopic transactionStatusTopic() {
+ return TopicBuilder.name(TRANSACTION_STATUS_TOPIC)
+ .partitions(3)
+ .replicas(1)
+ .build();
+ }
+}
diff --git a/anti-fraud-service/src/main/resources/application.properties b/anti-fraud-service/src/main/resources/application.properties
new file mode 100644
index 0000000000..e3ab175c3b
--- /dev/null
+++ b/anti-fraud-service/src/main/resources/application.properties
@@ -0,0 +1 @@
+spring.application.name=anti-fraud-service
diff --git a/anti-fraud-service/src/main/resources/application.yml b/anti-fraud-service/src/main/resources/application.yml
new file mode 100644
index 0000000000..87dab9a2cd
--- /dev/null
+++ b/anti-fraud-service/src/main/resources/application.yml
@@ -0,0 +1,21 @@
+spring:
+ application:
+ name: anti-fraud-service
+ kafka:
+ bootstrap-servers: ${SPRING_KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.apache.kafka.common.serialization.StringSerializer
+ consumer:
+ group-id: anti-fraud-group
+ auto-offset-reset: earliest
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+
+server:
+ port: ${PORT:8081}
+
+logging:
+ level:
+ root: INFO
+ com.yape.challenge: DEBUG
diff --git a/anti-fraud-service/src/test/java/com/yape/challenge/antifraud/AntiFraudServiceApplicationTests.java b/anti-fraud-service/src/test/java/com/yape/challenge/antifraud/AntiFraudServiceApplicationTests.java
new file mode 100644
index 0000000000..b1fd924db5
--- /dev/null
+++ b/anti-fraud-service/src/test/java/com/yape/challenge/antifraud/AntiFraudServiceApplicationTests.java
@@ -0,0 +1,13 @@
+package com.yape.challenge.antifraud;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class AntiFraudServiceApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/docker-compose.yml b/docker-compose.yml
index 0e8807f21c..82ac03a113 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -7,13 +7,22 @@ services:
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
+ pgadmin:
+ image: dpage/pgadmin4
+ environment:
+ - PGADMIN_DEFAULT_EMAIL=admin@admin.com
+ - PGADMIN_DEFAULT_PASSWORD=admin
+ ports:
+ - "5050:80"
+ depends_on:
+ - postgres
zookeeper:
image: confluentinc/cp-zookeeper:5.5.3
environment:
ZOOKEEPER_CLIENT_PORT: 2181
kafka:
image: confluentinc/cp-enterprise-kafka:5.5.3
- depends_on: [zookeeper]
+ depends_on: [ zookeeper ]
environment:
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
@@ -22,4 +31,34 @@ services:
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_JMX_PORT: 9991
ports:
- - 9092:9092
+ - "9092:9092"
+
+ redis:
+ image: redis:6.2-alpine
+ ports:
+ - "6379:6379"
+
+ transaction-service:
+ build:
+ context: ./transaction-service
+ ports:
+ - "8080:8080"
+ environment:
+ - SPRING_R2DBC_URL=r2dbc:postgresql://postgres:5432/postgres
+ - SPRING_R2DBC_USERNAME=postgres
+ - SPRING_R2DBC_PASSWORD=postgres
+ - SPRING_KAFKA_BOOTSTRAP_SERVERS=kafka:29092
+ - SPRING_DATA_REDIS_HOST=redis
+ - SPRING_DATA_REDIS_PORT=6379
+ depends_on:
+ - postgres
+ - kafka
+ - redis
+
+ anti-fraud-service:
+ build:
+ context: ./anti-fraud-service
+ environment:
+ - SPRING_KAFKA_BOOTSTRAP_SERVERS=kafka:29092
+ depends_on:
+ - kafka
diff --git a/transaction-service/.gitattributes b/transaction-service/.gitattributes
new file mode 100644
index 0000000000..8af972cded
--- /dev/null
+++ b/transaction-service/.gitattributes
@@ -0,0 +1,3 @@
+/gradlew text eol=lf
+*.bat text eol=crlf
+*.jar binary
diff --git a/transaction-service/.gitignore b/transaction-service/.gitignore
new file mode 100644
index 0000000000..c2065bc262
--- /dev/null
+++ b/transaction-service/.gitignore
@@ -0,0 +1,37 @@
+HELP.md
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
diff --git a/transaction-service/Dockerfile b/transaction-service/Dockerfile
new file mode 100644
index 0000000000..e4637c21b7
--- /dev/null
+++ b/transaction-service/Dockerfile
@@ -0,0 +1,16 @@
+# Stage 1: Build
+FROM gradle:8-jdk21 AS build
+WORKDIR /app
+COPY build.gradle settings.gradle ./
+COPY src ./src
+COPY gradle ./gradle
+COPY gradlew ./
+COPY gradlew.bat ./
+RUN chmod +x gradlew
+RUN ./gradlew build -x test
+
+# Stage 2: Run
+FROM eclipse-temurin:21-jre
+WORKDIR /app
+COPY --from=build /app/build/libs/*.jar app.jar
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/transaction-service/build.gradle b/transaction-service/build.gradle
new file mode 100644
index 0000000000..499085b3fd
--- /dev/null
+++ b/transaction-service/build.gradle
@@ -0,0 +1,45 @@
+plugins {
+ id 'java'
+ id 'org.springframework.boot' version '4.0.3'
+ id 'io.spring.dependency-management' version '1.1.7'
+}
+
+group = 'com.yape.challenge'
+version = '0.0.1-SNAPSHOT'
+description = 'Transaction service'
+
+java {
+ toolchain {
+ languageVersion = JavaLanguageVersion.of(21)
+ }
+}
+
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
+ implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive'
+ implementation 'org.springframework.boot:spring-boot-starter-kafka'
+ implementation 'org.springframework.boot:spring-boot-starter-webflux'
+ implementation 'org.springframework.boot:spring-boot-starter-graphql'
+ compileOnly 'org.projectlombok:lombok'
+ runtimeOnly 'org.postgresql:postgresql'
+ runtimeOnly 'org.postgresql:r2dbc-postgresql'
+ annotationProcessor 'org.projectlombok:lombok'
+ testImplementation 'org.springframework.boot:spring-boot-starter-data-r2dbc-test'
+ testImplementation 'org.springframework.boot:spring-boot-starter-kafka-test'
+ testImplementation 'org.springframework.boot:spring-boot-starter-webflux-test'
+ testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+}
+
+tasks.named('test') {
+ useJUnitPlatform()
+}
diff --git a/transaction-service/gradle/wrapper/gradle-wrapper.jar b/transaction-service/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000000..61285a659d
Binary files /dev/null and b/transaction-service/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/transaction-service/gradle/wrapper/gradle-wrapper.properties b/transaction-service/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000000..37f78a6af8
--- /dev/null
+++ b/transaction-service/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/transaction-service/gradlew b/transaction-service/gradlew
new file mode 100644
index 0000000000..adff685a03
--- /dev/null
+++ b/transaction-service/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+
+
+# Determine the Java command to use to start the JVM.
+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
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/transaction-service/gradlew.bat b/transaction-service/gradlew.bat
new file mode 100644
index 0000000000..c4bdd3ab8e
--- /dev/null
+++ b/transaction-service/gradlew.bat
@@ -0,0 +1,93 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/transaction-service/settings.gradle b/transaction-service/settings.gradle
new file mode 100644
index 0000000000..2d3e380cbb
--- /dev/null
+++ b/transaction-service/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'transaction-service'
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/TransactionServiceApplication.java b/transaction-service/src/main/java/com/yape/challenge/transaction/TransactionServiceApplication.java
new file mode 100644
index 0000000000..b06f337d85
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/TransactionServiceApplication.java
@@ -0,0 +1,13 @@
+package com.yape.challenge.transaction;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class TransactionServiceApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(TransactionServiceApplication.class, args);
+ }
+
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/application/usecase/CreateTransactionUseCase.java b/transaction-service/src/main/java/com/yape/challenge/transaction/application/usecase/CreateTransactionUseCase.java
new file mode 100644
index 0000000000..7f865c0b8d
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/application/usecase/CreateTransactionUseCase.java
@@ -0,0 +1,28 @@
+package com.yape.challenge.transaction.application.usecase;
+
+import com.yape.challenge.transaction.domain.messaging.EventPublisher;
+import com.yape.challenge.transaction.domain.model.Transaction;
+import com.yape.challenge.transaction.domain.model.TransactionStatus;
+import com.yape.challenge.transaction.domain.repository.TransactionRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+
+@Service
+@RequiredArgsConstructor
+public class CreateTransactionUseCase {
+
+ private final TransactionRepository transactionRepository;
+ private final EventPublisher eventPublisher;
+
+ public Mono execute(Transaction transaction) {
+ transaction.setStatus(TransactionStatus.PENDING);
+ transaction.setCreatedAt(LocalDateTime.now());
+
+ return transactionRepository.save(transaction)
+ .flatMap(savedTransaction -> eventPublisher.publishTransactionCreatedEvent(savedTransaction)
+ .thenReturn(savedTransaction));
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/application/usecase/GetTransactionUseCase.java b/transaction-service/src/main/java/com/yape/challenge/transaction/application/usecase/GetTransactionUseCase.java
new file mode 100644
index 0000000000..66397da7d2
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/application/usecase/GetTransactionUseCase.java
@@ -0,0 +1,20 @@
+package com.yape.challenge.transaction.application.usecase;
+
+import com.yape.challenge.transaction.domain.model.Transaction;
+import com.yape.challenge.transaction.domain.repository.TransactionRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+public class GetTransactionUseCase {
+
+ private final TransactionRepository transactionRepository;
+
+ public Mono execute(UUID id) {
+ return transactionRepository.findById(id);
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/application/usecase/UpdateTransactionStatusUseCase.java b/transaction-service/src/main/java/com/yape/challenge/transaction/application/usecase/UpdateTransactionStatusUseCase.java
new file mode 100644
index 0000000000..2ce866f30f
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/application/usecase/UpdateTransactionStatusUseCase.java
@@ -0,0 +1,21 @@
+package com.yape.challenge.transaction.application.usecase;
+
+import com.yape.challenge.transaction.domain.model.Transaction;
+import com.yape.challenge.transaction.domain.model.TransactionStatus;
+import com.yape.challenge.transaction.domain.repository.TransactionRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+@Service
+@RequiredArgsConstructor
+public class UpdateTransactionStatusUseCase {
+
+ private final TransactionRepository transactionRepository;
+
+ public Mono execute(UUID id, TransactionStatus status) {
+ return transactionRepository.updateStatus(id, status);
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/domain/messaging/EventPublisher.java b/transaction-service/src/main/java/com/yape/challenge/transaction/domain/messaging/EventPublisher.java
new file mode 100644
index 0000000000..4649bd6874
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/domain/messaging/EventPublisher.java
@@ -0,0 +1,8 @@
+package com.yape.challenge.transaction.domain.messaging;
+
+import com.yape.challenge.transaction.domain.model.Transaction;
+import reactor.core.publisher.Mono;
+
+public interface EventPublisher {
+ Mono publishTransactionCreatedEvent(Transaction transaction);
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/domain/model/Transaction.java b/transaction-service/src/main/java/com/yape/challenge/transaction/domain/model/Transaction.java
new file mode 100644
index 0000000000..4539003a1f
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/domain/model/Transaction.java
@@ -0,0 +1,23 @@
+package com.yape.challenge.transaction.domain.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Transaction {
+ private UUID id;
+ private UUID accountExternalIdDebit;
+ private UUID accountExternalIdCredit;
+ private Integer tranferTypeId;
+ private Double value;
+ private TransactionStatus status;
+ private LocalDateTime createdAt;
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/domain/model/TransactionStatus.java b/transaction-service/src/main/java/com/yape/challenge/transaction/domain/model/TransactionStatus.java
new file mode 100644
index 0000000000..e670311e28
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/domain/model/TransactionStatus.java
@@ -0,0 +1,7 @@
+package com.yape.challenge.transaction.domain.model;
+
+public enum TransactionStatus {
+ PENDING,
+ APPROVED,
+ REJECTED
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/domain/repository/TransactionRepository.java b/transaction-service/src/main/java/com/yape/challenge/transaction/domain/repository/TransactionRepository.java
new file mode 100644
index 0000000000..df67ff9855
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/domain/repository/TransactionRepository.java
@@ -0,0 +1,15 @@
+package com.yape.challenge.transaction.domain.repository;
+
+import com.yape.challenge.transaction.domain.model.Transaction;
+import com.yape.challenge.transaction.domain.model.TransactionStatus;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+public interface TransactionRepository {
+ Mono save(Transaction transaction);
+
+ Mono findById(UUID id);
+
+ Mono updateStatus(UUID id, TransactionStatus status);
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/graphql/TransactionGraphQLController.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/graphql/TransactionGraphQLController.java
new file mode 100644
index 0000000000..abda0d3579
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/graphql/TransactionGraphQLController.java
@@ -0,0 +1,41 @@
+package com.yape.challenge.transaction.infrastructure.adapters.in.graphql;
+
+import com.yape.challenge.transaction.application.usecase.CreateTransactionUseCase;
+import com.yape.challenge.transaction.application.usecase.GetTransactionUseCase;
+import com.yape.challenge.transaction.domain.model.Transaction;
+import com.yape.challenge.transaction.infrastructure.adapters.in.web.dto.CreateTransactionRequest;
+import com.yape.challenge.transaction.infrastructure.adapters.in.web.dto.TransactionResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.graphql.data.method.annotation.Argument;
+import org.springframework.graphql.data.method.annotation.MutationMapping;
+import org.springframework.graphql.data.method.annotation.QueryMapping;
+import org.springframework.stereotype.Controller;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+@Controller
+@RequiredArgsConstructor
+public class TransactionGraphQLController {
+
+ private final CreateTransactionUseCase createTransactionUseCase;
+ private final GetTransactionUseCase getTransactionUseCase;
+
+ @QueryMapping
+ public Mono getTransaction(@Argument String id) {
+ return getTransactionUseCase.execute(UUID.fromString(id))
+ .map(TransactionResponse::fromDomain);
+ }
+
+ @MutationMapping
+ public Mono createTransaction(@Argument("request") CreateTransactionRequest request) {
+ Transaction transaction = Transaction.builder()
+ .accountExternalIdDebit(request.getAccountExternalIdDebit())
+ .accountExternalIdCredit(request.getAccountExternalIdCredit())
+ .tranferTypeId(request.getTranferTypeId())
+ .value(request.getValue())
+ .build();
+ return createTransactionUseCase.execute(transaction)
+ .map(TransactionResponse::fromDomain);
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/kafka/TransactionStatusListener.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/kafka/TransactionStatusListener.java
new file mode 100644
index 0000000000..61556175d0
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/kafka/TransactionStatusListener.java
@@ -0,0 +1,36 @@
+package com.yape.challenge.transaction.infrastructure.adapters.in.kafka;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectMapper;
+import com.yape.challenge.transaction.application.usecase.UpdateTransactionStatusUseCase;
+import com.yape.challenge.transaction.infrastructure.adapters.in.kafka.dto.TransactionStatusUpdateEvent;
+import com.yape.challenge.transaction.infrastructure.config.KafkaTopicConfig;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TransactionStatusListener {
+
+ private final UpdateTransactionStatusUseCase updateTransactionStatusUseCase;
+ private final ObjectMapper objectMapper;
+
+ @KafkaListener(topics = KafkaTopicConfig.TRANSACTION_STATUS_TOPIC, groupId = "transaction-group")
+ public void consumeTransactionStatusEvent(String message) {
+ log.info("Received transaction status event: {}", message);
+ try {
+ TransactionStatusUpdateEvent event = objectMapper.readValue(message, TransactionStatusUpdateEvent.class);
+ updateTransactionStatusUseCase.execute(event.getTransactionId(), event.getStatus())
+ .doOnSuccess(t -> log.info("Successfully updated status to {} for transaction {}",
+ event.getStatus(), event.getTransactionId()))
+ .doOnError(e -> log.error("Failed to update transaction status", e))
+ .subscribe();
+
+ } catch (JacksonException e) {
+ log.error("Failed to deserialize transaction status event", e);
+ }
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/kafka/dto/TransactionStatusUpdateEvent.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/kafka/dto/TransactionStatusUpdateEvent.java
new file mode 100644
index 0000000000..be627eefe0
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/kafka/dto/TransactionStatusUpdateEvent.java
@@ -0,0 +1,12 @@
+package com.yape.challenge.transaction.infrastructure.adapters.in.kafka.dto;
+
+import com.yape.challenge.transaction.domain.model.TransactionStatus;
+import lombok.Data;
+
+import java.util.UUID;
+
+@Data
+public class TransactionStatusUpdateEvent {
+ private UUID transactionId;
+ private TransactionStatus status;
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/TransactionHandler.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/TransactionHandler.java
new file mode 100644
index 0000000000..5620ba32f5
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/TransactionHandler.java
@@ -0,0 +1,51 @@
+package com.yape.challenge.transaction.infrastructure.adapters.in.web;
+
+import com.yape.challenge.transaction.application.usecase.CreateTransactionUseCase;
+import com.yape.challenge.transaction.application.usecase.GetTransactionUseCase;
+import com.yape.challenge.transaction.domain.model.Transaction;
+import com.yape.challenge.transaction.infrastructure.adapters.in.web.dto.CreateTransactionRequest;
+import com.yape.challenge.transaction.infrastructure.adapters.in.web.dto.TransactionResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.server.ServerRequest;
+import org.springframework.web.reactive.function.server.ServerResponse;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+@Component
+@RequiredArgsConstructor
+public class TransactionHandler {
+
+ private final CreateTransactionUseCase createTransactionUseCase;
+ private final GetTransactionUseCase getTransactionUseCase;
+
+ public Mono createTransaction(ServerRequest request) {
+ return request.bodyToMono(CreateTransactionRequest.class)
+ .map(req -> Transaction.builder()
+ .accountExternalIdDebit(req.getAccountExternalIdDebit())
+ .accountExternalIdCredit(req.getAccountExternalIdCredit())
+ .tranferTypeId(req.getTranferTypeId())
+ .value(req.getValue())
+ .build())
+ .flatMap(createTransactionUseCase::execute)
+ .map(TransactionResponse::fromDomain)
+ .flatMap(response -> ServerResponse.ok().bodyValue(response))
+ .onErrorResume(e -> ServerResponse.badRequest().bodyValue(e.getMessage()));
+ }
+
+ public Mono getTransaction(ServerRequest request) {
+ String idStr = request.pathVariable("id");
+ UUID id;
+ try {
+ id = UUID.fromString(idStr);
+ } catch (IllegalArgumentException e) {
+ return ServerResponse.badRequest().bodyValue("Invalid UUID format");
+ }
+
+ return getTransactionUseCase.execute(id)
+ .map(TransactionResponse::fromDomain)
+ .flatMap(response -> ServerResponse.ok().bodyValue(response))
+ .switchIfEmpty(ServerResponse.notFound().build());
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/TransactionRouter.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/TransactionRouter.java
new file mode 100644
index 0000000000..fe201eaaff
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/TransactionRouter.java
@@ -0,0 +1,20 @@
+package com.yape.challenge.transaction.infrastructure.adapters.in.web;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.reactive.function.server.RouterFunction;
+import org.springframework.web.reactive.function.server.ServerResponse;
+
+import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
+import static org.springframework.web.reactive.function.server.RequestPredicates.POST;
+import static org.springframework.web.reactive.function.server.RouterFunctions.route;
+
+@Configuration
+public class TransactionRouter {
+
+ @Bean
+ public RouterFunction transactionRoutes(TransactionHandler handler) {
+ return route(POST("/api/transactions"), handler::createTransaction)
+ .andRoute(GET("/api/transactions/{id}"), handler::getTransaction);
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/dto/CreateTransactionRequest.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/dto/CreateTransactionRequest.java
new file mode 100644
index 0000000000..48af1a2448
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/dto/CreateTransactionRequest.java
@@ -0,0 +1,13 @@
+package com.yape.challenge.transaction.infrastructure.adapters.in.web.dto;
+
+import lombok.Data;
+
+import java.util.UUID;
+
+@Data
+public class CreateTransactionRequest {
+ private UUID accountExternalIdDebit;
+ private UUID accountExternalIdCredit;
+ private Integer tranferTypeId;
+ private Double value;
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/dto/TransactionResponse.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/dto/TransactionResponse.java
new file mode 100644
index 0000000000..42d0de88ea
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/in/web/dto/TransactionResponse.java
@@ -0,0 +1,47 @@
+package com.yape.challenge.transaction.infrastructure.adapters.in.web.dto;
+
+import com.yape.challenge.transaction.domain.model.Transaction;
+import lombok.Builder;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Data
+@Builder
+public class TransactionResponse {
+ private UUID transactionExternalId;
+ private TransactionTypeResponse transactionType;
+ private TransactionStatusResponse transactionStatus;
+ private Double value;
+ private LocalDateTime createdAt;
+
+ @Data
+ @Builder
+ public static class TransactionTypeResponse {
+ private String name;
+ }
+
+ @Data
+ @Builder
+ public static class TransactionStatusResponse {
+ private String name;
+ }
+
+ public static TransactionResponse fromDomain(Transaction transaction) {
+ if (transaction == null)
+ return null;
+
+ return TransactionResponse.builder()
+ .transactionExternalId(transaction.getId())
+ .transactionType(TransactionTypeResponse.builder()
+ .name(String.valueOf(transaction.getTranferTypeId())) // Or map to actual name if needed
+ .build())
+ .transactionStatus(TransactionStatusResponse.builder()
+ .name(transaction.getStatus().name())
+ .build())
+ .value(transaction.getValue())
+ .createdAt(transaction.getCreatedAt())
+ .build();
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/database/DatabaseTransactionRepositoryAdapter.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/database/DatabaseTransactionRepositoryAdapter.java
new file mode 100644
index 0000000000..41667cda62
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/database/DatabaseTransactionRepositoryAdapter.java
@@ -0,0 +1,112 @@
+package com.yape.challenge.transaction.infrastructure.adapters.out.database;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectMapper;
+import com.yape.challenge.transaction.domain.model.Transaction;
+import com.yape.challenge.transaction.domain.model.TransactionStatus;
+import com.yape.challenge.transaction.domain.repository.TransactionRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.ReactiveStringRedisTemplate;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class DatabaseTransactionRepositoryAdapter implements TransactionRepository {
+
+ private final TransactionR2dbcRepository repository;
+ private final ReactiveStringRedisTemplate redisTemplate;
+ private final ObjectMapper objectMapper;
+
+ private static final String CACHE_KEY_PREFIX = "transaction:";
+
+ @Override
+ public Mono save(Transaction transaction) {
+ TransactionEntity entity = TransactionEntity.builder()
+ .id(transaction.getId())
+ .accountExternalIdDebit(transaction.getAccountExternalIdDebit())
+ .accountExternalIdCredit(transaction.getAccountExternalIdCredit())
+ .tranferTypeId(transaction.getTranferTypeId())
+ .value(transaction.getValue())
+ .status(transaction.getStatus())
+ .createdAt(transaction.getCreatedAt())
+ .build();
+
+ return repository.save(entity)
+ .flatMap(this::saveToCache)
+ .map(this::mapToDomain);
+ }
+
+ @Override
+ public Mono findById(UUID id) {
+ return getFromCache(id)
+ .doOnNext(e -> log.info("[CACHE HIT] Transaction {} retrieved from Redis", id))
+ .switchIfEmpty(
+ repository.findById(id)
+ .doOnNext(e -> log.info("[CACHE MISS] Transaction {} retrieved from PostgreSQL", id))
+ .flatMap(this::saveToCache))
+ .map(this::mapToDomain);
+ }
+
+ @Override
+ public Mono updateStatus(UUID id, TransactionStatus status) {
+ return repository.findById(id)
+ .flatMap(entity -> {
+ entity.setStatus(status);
+ return repository.save(entity);
+ })
+ .flatMap(this::saveToCache)
+ .map(this::mapToDomain);
+ }
+
+ private Mono saveToCache(TransactionEntity entity) {
+ try {
+ String json = objectMapper.writeValueAsString(entity);
+ return redisTemplate.opsForValue()
+ .set(CACHE_KEY_PREFIX + entity.getId(), json)
+ .thenReturn(entity)
+ .onErrorResume(e -> {
+ log.error("Failed to save transaction to Redis cache", e);
+ return Mono.just(entity);
+ });
+ } catch (JacksonException e) {
+ log.error("Failed to serialize transaction for Redis cache", e);
+ return Mono.just(entity);
+ }
+ }
+
+ private Mono getFromCache(UUID id) {
+ return redisTemplate.opsForValue()
+ .get(CACHE_KEY_PREFIX + id)
+ .flatMap(json -> {
+ try {
+ TransactionEntity entity = objectMapper.readValue(json, TransactionEntity.class);
+ return Mono.just(entity);
+ } catch (JacksonException e) {
+ log.error("[CACHE ERROR] Failed to deserialize transaction {} from Redis, falling back to DB",
+ id, e);
+ return Mono.empty();
+ }
+ })
+ .onErrorResume(e -> {
+ log.error("[CACHE ERROR] Failed to connect to Redis for transaction {}, falling back to DB", id, e);
+ return Mono.empty();
+ });
+ }
+
+ private Transaction mapToDomain(TransactionEntity entity) {
+ return Transaction.builder()
+ .id(entity.getId())
+ .accountExternalIdDebit(entity.getAccountExternalIdDebit())
+ .accountExternalIdCredit(entity.getAccountExternalIdCredit())
+ .tranferTypeId(entity.getTranferTypeId())
+ .value(entity.getValue())
+ .status(entity.getStatus())
+ .createdAt(entity.getCreatedAt())
+ .build();
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/database/TransactionEntity.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/database/TransactionEntity.java
new file mode 100644
index 0000000000..8f421dd523
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/database/TransactionEntity.java
@@ -0,0 +1,28 @@
+package com.yape.challenge.transaction.infrastructure.adapters.out.database;
+
+import com.yape.challenge.transaction.domain.model.TransactionStatus;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+@Table("transactions")
+public class TransactionEntity {
+ @Id
+ private UUID id;
+ private UUID accountExternalIdDebit;
+ private UUID accountExternalIdCredit;
+ private Integer tranferTypeId;
+ private Double value;
+ private TransactionStatus status;
+ private LocalDateTime createdAt;
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/database/TransactionR2dbcRepository.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/database/TransactionR2dbcRepository.java
new file mode 100644
index 0000000000..8da5e7fc64
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/database/TransactionR2dbcRepository.java
@@ -0,0 +1,10 @@
+package com.yape.challenge.transaction.infrastructure.adapters.out.database;
+
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.UUID;
+
+@Repository
+public interface TransactionR2dbcRepository extends ReactiveCrudRepository {
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/kafka/KafkaEventPublisherAdapter.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/kafka/KafkaEventPublisherAdapter.java
new file mode 100644
index 0000000000..bc3e081b6e
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/kafka/KafkaEventPublisherAdapter.java
@@ -0,0 +1,41 @@
+package com.yape.challenge.transaction.infrastructure.adapters.out.kafka;
+
+import tools.jackson.core.JacksonException;
+import tools.jackson.databind.ObjectMapper;
+import com.yape.challenge.transaction.domain.messaging.EventPublisher;
+import com.yape.challenge.transaction.domain.model.Transaction;
+import com.yape.challenge.transaction.infrastructure.adapters.out.kafka.dto.TransactionCreatedEvent;
+import com.yape.challenge.transaction.infrastructure.config.KafkaTopicConfig;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class KafkaEventPublisherAdapter implements EventPublisher {
+
+ private final KafkaTemplate kafkaTemplate;
+ private final ObjectMapper objectMapper;
+
+ @Override
+ public Mono publishTransactionCreatedEvent(Transaction transaction) {
+ return Mono.fromRunnable(() -> {
+ try {
+ TransactionCreatedEvent event = TransactionCreatedEvent.builder()
+ .transactionId(transaction.getId())
+ .value(transaction.getValue())
+ .build();
+
+ String payload = objectMapper.writeValueAsString(event);
+ kafkaTemplate.send(KafkaTopicConfig.TRANSACTION_CREATED_TOPIC, transaction.getId().toString(), payload);
+ log.info("Published TransactionCreatedEvent for ID: {}", transaction.getId());
+
+ } catch (JacksonException e) {
+ log.error("Error serializing TransactionCreatedEvent", e);
+ }
+ });
+ }
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/kafka/dto/TransactionCreatedEvent.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/kafka/dto/TransactionCreatedEvent.java
new file mode 100644
index 0000000000..3189e36074
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/adapters/out/kafka/dto/TransactionCreatedEvent.java
@@ -0,0 +1,13 @@
+package com.yape.challenge.transaction.infrastructure.adapters.out.kafka.dto;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.UUID;
+
+@Data
+@Builder
+public class TransactionCreatedEvent {
+ private UUID transactionId;
+ private Double value;
+}
diff --git a/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/config/KafkaTopicConfig.java b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/config/KafkaTopicConfig.java
new file mode 100644
index 0000000000..759f2c6369
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/challenge/transaction/infrastructure/config/KafkaTopicConfig.java
@@ -0,0 +1,29 @@
+package com.yape.challenge.transaction.infrastructure.config;
+
+import org.apache.kafka.clients.admin.NewTopic;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.config.TopicBuilder;
+
+@Configuration
+public class KafkaTopicConfig {
+
+ public static final String TRANSACTION_CREATED_TOPIC = "transaction-created";
+ public static final String TRANSACTION_STATUS_TOPIC = "transaction-status";
+
+ @Bean
+ public NewTopic transactionCreatedTopic() {
+ return TopicBuilder.name(TRANSACTION_CREATED_TOPIC)
+ .partitions(3)
+ .replicas(1)
+ .build();
+ }
+
+ @Bean
+ public NewTopic transactionStatusTopic() {
+ return TopicBuilder.name(TRANSACTION_STATUS_TOPIC)
+ .partitions(3)
+ .replicas(1)
+ .build();
+ }
+}
diff --git a/transaction-service/src/main/resources/application.yml b/transaction-service/src/main/resources/application.yml
new file mode 100644
index 0000000000..d7d801c345
--- /dev/null
+++ b/transaction-service/src/main/resources/application.yml
@@ -0,0 +1,36 @@
+spring:
+ application:
+ name: transaction-service
+ r2dbc:
+ url: ${SPRING_R2DBC_URL:r2dbc:postgresql://localhost:5432/postgres}
+ username: ${SPRING_R2DBC_USERNAME:postgres}
+ password: ${SPRING_R2DBC_PASSWORD:postgres}
+ sql:
+ init:
+ mode: always
+ schema-locations: classpath:schema.sql
+ kafka:
+ bootstrap-servers: ${SPRING_KAFKA_BOOTSTRAP_SERVERS:localhost:9092}
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.apache.kafka.common.serialization.StringSerializer
+ consumer:
+ group-id: transaction-group
+ auto-offset-reset: earliest
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ graphql:
+ graphiql:
+ enabled: true
+ data:
+ redis:
+ host: ${SPRING_DATA_REDIS_HOST:localhost}
+ port: ${SPRING_DATA_REDIS_PORT:6379}
+server:
+ port: ${PORT:8080}
+
+logging:
+ level:
+ root: INFO
+ com.yape.challenge: DEBUG
+ org.springframework.data.r2dbc: DEBUG
diff --git a/transaction-service/src/main/resources/graphql/schema.graphqls b/transaction-service/src/main/resources/graphql/schema.graphqls
new file mode 100644
index 0000000000..4f1978f4f7
--- /dev/null
+++ b/transaction-service/src/main/resources/graphql/schema.graphqls
@@ -0,0 +1,30 @@
+type Query {
+ getTransaction(id: ID!): TransactionResponse
+}
+
+type Mutation {
+ createTransaction(request: CreateTransactionRequestInput!): TransactionResponse
+}
+
+input CreateTransactionRequestInput {
+ accountExternalIdDebit: String!
+ accountExternalIdCredit: String!
+ tranferTypeId: Int!
+ value: Float!
+}
+
+type TransactionResponse {
+ transactionExternalId: ID
+ transactionType: TransactionTypeResponse
+ transactionStatus: TransactionStatusResponse
+ value: Float
+ createdAt: String
+}
+
+type TransactionTypeResponse {
+ name: String
+}
+
+type TransactionStatusResponse {
+ name: String
+}
diff --git a/transaction-service/src/main/resources/schema.sql b/transaction-service/src/main/resources/schema.sql
new file mode 100644
index 0000000000..0cdde4376f
--- /dev/null
+++ b/transaction-service/src/main/resources/schema.sql
@@ -0,0 +1,11 @@
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+
+CREATE TABLE IF NOT EXISTS transactions (
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
+ account_external_id_debit UUID NOT NULL,
+ account_external_id_credit UUID NOT NULL,
+ tranfer_type_id INT NOT NULL,
+ value DOUBLE PRECISION NOT NULL,
+ status VARCHAR(50) NOT NULL,
+ created_at TIMESTAMP NOT NULL
+);
diff --git a/transaction-service/src/test/java/com/yape/challenge/transaction/TransactionServiceApplicationTests.java b/transaction-service/src/test/java/com/yape/challenge/transaction/TransactionServiceApplicationTests.java
new file mode 100644
index 0000000000..3d4949243d
--- /dev/null
+++ b/transaction-service/src/test/java/com/yape/challenge/transaction/TransactionServiceApplicationTests.java
@@ -0,0 +1,13 @@
+package com.yape.challenge.transaction;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class TransactionServiceApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}