diff --git a/.gitignore b/.gitignore
index 67045665db..892c767017 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,104 +1,57 @@
-# Logs
-logs
+# Maven
+target/
+pom.xml.tag
+pom.xml.releaseBackup
+pom.xml.versionsBackup
+pom.xml.next
+release.properties
+dependency-reduced-pom.xml
+buildNumber.properties
+.mvn/timing.properties
+.mvn/wrapper/maven-wrapper.jar
+
+# Java
+*.class
*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
-
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
-
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# TypeScript v1 declaration files
-typings/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+hs_err_pid*
+replay_pid*
+
+# IDE
+.idea/
+*.iws
+*.iml
+*.ipr
+.vscode/
+.classpath
+.project
+.settings/
+bin/
+
+# Spring Boot
+spring-output/
+
+# Docker
+.docker/
+
+# OS
+.DS_Store
+Thumbs.db
+*.swp
+*.swo
+*~
-# Optional eslint cache
-.eslintcache
-
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
+# Logs
+logs/
+*.log
-# dotenv environment variables file
+# Environment
.env
-.env.test
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-
-# Next.js build output
-.next
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and *not* Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
+.env.local
-# TernJS port file
-.tern-port
diff --git a/README.md b/README.md
index b067a71026..5e5b99581b 100644
--- a/README.md
+++ b/README.md
@@ -1,82 +1,318 @@
-# Yape Code Challenge :rocket:
+# Sistema de Transacciones con Validación Anti-Fraude
-Our code challenge will let you marvel us with your Jedi coding skills :smile:.
+## Descripción
-Don't forget that the proper way to submit your work is to fork the repo and create a PR :wink: ... have fun !!
+Sistema de transacciones financieras con validación anti-fraude en tiempo real, implementado con arquitectura de microservicios y comunicación asíncrona mediante Apache Kafka.
-- [Problem](#problem)
-- [Tech Stack](#tech_stack)
-- [Send us your challenge](#send_us_your_challenge)
+### Arquitectura
-# Problem
+```
+Cliente → Transaction Service (REST API)
+ ↓ (guarda en PostgreSQL)
+ ↓ (publica evento en Kafka)
+ Anti-Fraud Service
+ ↓ (valida: value > 1000 = reject)
+ ↓ (publica resultado en Kafka)
+ Transaction Service
+ ↓ (actualiza estado en PostgreSQL)
+```
+
+### Stack Tecnológico
+
+- **Java 17**
+- **Spring Boot 3.2.1**
+- **PostgreSQL 14**
+- **Apache Kafka**
+- **Maven 3.9+**
+- **Docker & Docker Compose**
+
+---
+
+## Inicio Rápido
+
+### Prerrequisitos
+
+```bash
+- Java 17 o superior (JDK)
+- Maven 3.9+
+- Docker Desktop
+- Git
+```
+
+### 1. Levantar Infraestructura (PostgreSQL + Kafka)
+
+```powershell
+# Desde el directorio raíz del proyecto
+docker-compose up -d
+
+# Verificar que los servicios estén corriendo
+docker-compose ps
+
+# Verificar logs
+docker-compose logs -f
+```
+
+Servicios disponibles:
+- PostgreSQL: `localhost:5432`
+- Kafka: `localhost:9092`
+- Zookeeper: `localhost:2181`
+
+### 2. Compilar el Proyecto
+
+```powershell
+# Compilar todos los módulos
+mvn clean install
+```
+
+### 3. Ejecutar Transaction Service
+
+```powershell
+# Opción 1: Desde Maven
+cd transaction-service
+mvn spring-boot:run
+
+# Opción 2: Desde JAR compilado
+cd transaction-service\target
+java -jar transaction-service-1.0.0.jar
+```
-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:
+El servicio estará disponible en: `http://localhost:8080`
-
- - pending
- - approved
- - rejected
-
+### 4. Ejecutar Anti-Fraud Service (en otra terminal)
-Every transaction with a value greater than 1000 should be rejected.
+```powershell
+# Opción 1: Desde Maven
+cd anti-fraud-service
+mvn spring-boot:run
-```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)]
+# Opción 2: Desde JAR compilado
+cd anti-fraud-service\target
+java -jar anti-fraud-service-1.0.0.jar
```
-# Tech Stack
+El servicio estará disponible en: `http://localhost:8081`
+
+---
-
- - Node. You can use any framework you want (i.e. Nestjs with an ORM like TypeOrm or Prisma)
- - Any database
- - Kafka
-
+## API Endpoints
-We do provide a `Dockerfile` to help you get started with a dev environment.
+### Base URL
+```
+http://localhost:8080/api/v1
+```
-You must have two resources:
+### 1. Crear Transacción
-1. Resource to create a transaction that must containt:
+**POST** `/transactions`
+**Request Body:**
```json
{
- "accountExternalIdDebit": "Guid",
- "accountExternalIdCredit": "Guid",
+ "accountExternalIdDebit": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "accountExternalIdCredit": "7ba95f64-8821-4562-b3fc-2c963f66afa9",
"tranferTypeId": 1,
- "value": 120
+ "value": 500.50
}
```
-2. Resource to retrieve a transaction
+**Response (201 Created):**
+```json
+{
+ "transactionExternalId": "9ca85f64-1234-4562-b3fc-2c963f66afa1",
+ "transactionType": {
+ "name": "transfer"
+ },
+ "transactionStatus": {
+ "name": "pending"
+ },
+ "value": 500.50,
+ "createdAt": "2026-01-03T10:30:00"
+}
+```
+
+### 2. Obtener Transacción
+
+**GET** `/transactions/{transactionExternalId}`
+**Response (200 OK):**
```json
{
- "transactionExternalId": "Guid",
+ "transactionExternalId": "9ca85f64-1234-4562-b3fc-2c963f66afa1",
"transactionType": {
- "name": ""
+ "name": "transfer"
},
"transactionStatus": {
- "name": ""
+ "name": "approved"
},
- "value": 120,
- "createdAt": "Date"
+ "value": 500.50,
+ "createdAt": "2026-01-03T10:30:00"
+}
+```
+
+**Response (404 Not Found):**
+```json
+{
+ "error": "Transaction not found",
+ "message": "Transaction not found with id: 9ca85f64-1234-4562-b3fc-2c963f66afa1",
+ "timestamp": "2026-01-03T10:30:00Z"
}
```
-## Optional
+---
+
+## Testing Manual con cURL
+
+### Crear transacción APROBADA (value <= 1000)
+
+```powershell
+curl -X POST http://localhost:8080/api/v1/transactions `
+ -H "Content-Type: application/json" `
+ -d '{
+ "accountExternalIdDebit": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "accountExternalIdCredit": "7ba95f64-8821-4562-b3fc-2c963f66afa9",
+ "tranferTypeId": 1,
+ "value": 800.50
+ }'
+```
+
+### Crear transacción RECHAZADA (value > 1000)
+
+```powershell
+curl -X POST http://localhost:8080/api/v1/transactions `
+ -H "Content-Type: application/json" `
+ -d '{
+ "accountExternalIdDebit": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "accountExternalIdCredit": "7ba95f64-8821-4562-b3fc-2c963f66afa9",
+ "tranferTypeId": 1,
+ "value": 1500.00
+ }'
+```
+
+### Consultar transacción
+
+```powershell
+# Reemplazar {UUID} con el transactionExternalId recibido
+curl http://localhost:8080/api/v1/transactions/{UUID}
+```
+
+---
+
+## Flujo de Procesamiento
+
+1. **Cliente crea transacción** → POST a Transaction Service
+2. **Transaction Service:**
+ - Valida datos
+ - Guarda transacción con estado `pending` en PostgreSQL
+ - Publica evento `transaction-created` en Kafka
+ - Retorna respuesta al cliente
+3. **Anti-Fraud Service:**
+ - Consume evento `transaction-created`
+ - Aplica regla: `value > 1000 ? "rejected" : "approved"`
+ - Publica evento `transaction-status-updated` en Kafka
+4. **Transaction Service:**
+ - Consume evento `transaction-status-updated`
+ - Actualiza estado de transacción en PostgreSQL
+5. **Cliente consulta transacción** → GET a Transaction Service
+ - Retorna transacción con estado actualizado
+
+---
+
+## Arquitectura del Código
+
+### Transaction Service
+
+```
+transaction-service/
+├── domain/ # Lógica de negocio pura
+│ ├── model/ # Entidades de dominio
+│ ├── port/ # Interfaces (Hexagonal)
+│ └── exception/ # Excepciones de dominio
+├── application/ # Casos de uso
+│ ├── dto/ # DTOs de aplicación
+│ └── service/ # Servicios de aplicación
+└── infrastructure/ # Adaptadores
+ ├── api/ # REST Controllers
+ ├── persistence/ # JPA + PostgreSQL
+ └── messaging/ # Kafka Producer/Consumer
+```
+
+### Anti-Fraud Service
+
+```
+anti-fraud-service/
+├── domain/ # Lógica de validación
+│ └── service/ # FraudValidatorService
+└── infrastructure/ # Adaptadores
+ └── messaging/ # Kafka Consumer/Producer
+ ├── consumer/
+ ├── producer/
+ └── config/
+```
+
+---
+
+## Modelo de Datos
+
+### Estados de Transacción
+- `pending`: Estado inicial al crear la transacción
+- `approved`: Transacción aprobada por anti-fraude (value <= 1000)
+- `rejected`: Transacción rechazada por anti-fraude (value > 1000)
+
+### Tipos de Transacción
+- `1`: Transfer
+- `2`: Deposit
+- `3`: Withdrawal
+
+---
+
+## Health Checks
+
+### Transaction Service
+```bash
+curl http://localhost:8080/actuator/health
+```
+
+### Anti-Fraud Service
+```bash
+curl http://localhost:8081/actuator/health
+```
+
+
+
+## Limpieza
+
+```powershell
+# Detener servicios
+docker-compose down
+
+# Detener servicios y eliminar volúmenes (borra datos)
+docker-compose down -v
+
+# Limpiar build de Maven
+mvn clean
+```
+
+---
+
+## Características Implementadas
+
+- ✅ Clean Architecture / Hexagonal Pattern
+- ✅ Separación de capas (Domain, Application, Infrastructure)
+- ✅ Comunicación asíncrona con Kafka
+- ✅ Persistencia con PostgreSQL + Flyway migrations
+- ✅ Validación de datos con Bean Validation
+- ✅ Manejo de errores global
+- ✅ Optimistic Locking para concurrencia
+- ✅ Health checks con Spring Actuator
+- ✅ Logging estructurado
+- ✅ Configuración por ambientes
+- ✅ Docker Compose para infraestructura
-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;
+## Autor
-# Send us your challenge
+**Ricardo** - Reto Técnico Yape Backend Java
-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/anti-fraud-service/pom.xml b/anti-fraud-service/pom.xml
new file mode 100644
index 0000000000..59d2fccaba
--- /dev/null
+++ b/anti-fraud-service/pom.xml
@@ -0,0 +1,66 @@
+
+
+ 4.0.0
+
+
+ com.yape
+ transaction-system
+ 1.0.0
+
+
+ anti-fraud-service
+ Anti-Fraud Service
+ Servicio de validación anti-fraude
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.kafka
+ spring-kafka-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+
+
diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/AntiFraudServiceApplication.java b/anti-fraud-service/src/main/java/com/yape/antifraud/AntiFraudServiceApplication.java
new file mode 100644
index 0000000000..5c25b612c8
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/antifraud/AntiFraudServiceApplication.java
@@ -0,0 +1,18 @@
+package com.yape.antifraud;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.kafka.annotation.EnableKafka;
+
+/**
+ * Aplicación principal - Anti-Fraud Service
+ */
+@EnableKafka
+@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/antifraud/domain/service/FraudValidatorService.java b/anti-fraud-service/src/main/java/com/yape/antifraud/domain/service/FraudValidatorService.java
new file mode 100644
index 0000000000..883eef353b
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/antifraud/domain/service/FraudValidatorService.java
@@ -0,0 +1,59 @@
+package com.yape.antifraud.domain.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+/**
+ * Servicio de validación anti-fraude
+ */
+@Slf4j
+@Service
+public class FraudValidatorService {
+
+ @Value("${anti-fraud.max-transaction-value}")
+ private BigDecimal maxTransactionValue;
+
+ /**
+ * Valida si una transacción es fraudulenta
+ * Regla: Si el valor es mayor a 1000, se rechaza
+ */
+ public ValidationResult validate(UUID transactionId, BigDecimal transactionValue) {
+ log.info("Validating transaction {} with value: {}", transactionId, transactionValue);
+
+ if (transactionValue.compareTo(maxTransactionValue) > 0) {
+ log.warn("Transaction {} REJECTED - Value {} exceeds threshold {}",
+ transactionId, transactionValue, maxTransactionValue);
+ return ValidationResult.rejected(
+ String.format("Transaction value %.2f exceeds fraud threshold (%.2f)",
+ transactionValue, maxTransactionValue)
+ );
+ }
+
+ log.info("Transaction {} APPROVED - Value {} is within threshold",
+ transactionId, transactionValue);
+ return ValidationResult.approved("Transaction passed fraud validation");
+ }
+
+ /**
+ * Resultado de la validación
+ */
+ @lombok.Data
+ @lombok.AllArgsConstructor
+ public static class ValidationResult {
+ private String status; // "approved" or "rejected"
+ private String reason;
+
+ public static ValidationResult approved(String reason) {
+ return new ValidationResult("approved", reason);
+ }
+
+ public static ValidationResult rejected(String reason) {
+ return new ValidationResult("rejected", reason);
+ }
+ }
+}
+
diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/config/KafkaConfig.java b/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/config/KafkaConfig.java
new file mode 100644
index 0000000000..bad784dc23
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/config/KafkaConfig.java
@@ -0,0 +1,77 @@
+package com.yape.antifraud.infrastructure.messaging.config;
+
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.common.serialization.StringDeserializer;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.annotation.EnableKafka;
+import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.core.*;
+import org.springframework.kafka.listener.ContainerProperties;
+import org.springframework.kafka.support.serializer.JsonDeserializer;
+import org.springframework.kafka.support.serializer.JsonSerializer;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Configuración de Kafka para Anti-Fraud Service
+ */
+@EnableKafka
+@Configuration
+public class KafkaConfig {
+
+ @Value("${spring.kafka.bootstrap-servers}")
+ private String bootstrapServers;
+
+ @Value("${spring.kafka.consumer.group-id}")
+ private String groupId;
+
+ // Producer Configuration
+ @Bean
+ public ProducerFactory producerFactory() {
+ Map config = new HashMap<>();
+ config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
+ config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
+ config.put(ProducerConfig.ACKS_CONFIG, "all");
+ config.put(ProducerConfig.RETRIES_CONFIG, 3);
+ config.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
+ config.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false);
+ return new DefaultKafkaProducerFactory<>(config);
+ }
+
+ @Bean
+ public KafkaTemplate kafkaTemplate() {
+ return new KafkaTemplate<>(producerFactory());
+ }
+
+ // Consumer Configuration
+ @Bean
+ public ConsumerFactory consumerFactory() {
+ Map config = new HashMap<>();
+ config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ config.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
+ config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+ config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
+ config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
+ config.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
+ config.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
+ config.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false);
+ config.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "com.yape.antifraud.infrastructure.messaging.event.TransactionCreatedEvent");
+ return new DefaultKafkaConsumerFactory<>(config);
+ }
+
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory =
+ new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
+ return factory;
+ }
+}
+
diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/consumer/TransactionCreatedConsumer.java b/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/consumer/TransactionCreatedConsumer.java
new file mode 100644
index 0000000000..587e984928
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/consumer/TransactionCreatedConsumer.java
@@ -0,0 +1,64 @@
+package com.yape.antifraud.infrastructure.messaging.consumer;
+
+import com.yape.antifraud.domain.service.FraudValidatorService;
+import com.yape.antifraud.infrastructure.messaging.event.TransactionCreatedEvent;
+import com.yape.antifraud.infrastructure.messaging.event.TransactionStatusUpdatedEvent;
+import com.yape.antifraud.infrastructure.messaging.producer.FraudResultProducer;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.support.Acknowledgment;
+import org.springframework.stereotype.Component;
+
+import java.util.UUID;
+
+/**
+ * Consumer de Kafka para eventos de transacciones creadas
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TransactionCreatedConsumer {
+
+ private final FraudValidatorService fraudValidatorService;
+ private final FraudResultProducer fraudResultProducer;
+
+ @KafkaListener(
+ topics = "${kafka.topics.transaction-created}",
+ groupId = "${spring.kafka.consumer.group-id}",
+ containerFactory = "kafkaListenerContainerFactory"
+ )
+ public void consume(TransactionCreatedEvent event, Acknowledgment acknowledgment) {
+ try {
+ log.info("Received TransactionCreatedEvent: {}", event);
+
+ // Validar transacción
+ FraudValidatorService.ValidationResult result = fraudValidatorService.validate(
+ event.getTransactionExternalId(),
+ event.getValue()
+ );
+
+ // Publicar resultado
+ TransactionStatusUpdatedEvent statusEvent = TransactionStatusUpdatedEvent.builder()
+ .eventId(UUID.randomUUID().toString())
+ .eventType("TRANSACTION_STATUS_UPDATED")
+ .timestamp(java.time.Instant.now().toString())
+ .correlationId(event.getTransactionExternalId())
+ .transactionExternalId(event.getTransactionExternalId())
+ .newStatus(result.getStatus())
+ .reason(result.getReason())
+ .build();
+
+ fraudResultProducer.publishValidationResult(statusEvent);
+
+ // Confirmar mensaje procesado
+ acknowledgment.acknowledge();
+ log.info("TransactionCreatedEvent processed successfully");
+
+ } catch (Exception e) {
+ log.error("Error processing TransactionCreatedEvent: {}", event, e);
+ acknowledgment.acknowledge(); // Confirmar para evitar loops infinitos
+ }
+ }
+}
+
diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/event/TransactionCreatedEvent.java b/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/event/TransactionCreatedEvent.java
new file mode 100644
index 0000000000..6ded1e13d5
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/event/TransactionCreatedEvent.java
@@ -0,0 +1,28 @@
+package com.yape.antifraud.infrastructure.messaging.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+/**
+ * Evento recibido cuando se crea una transacción
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TransactionCreatedEvent {
+ private String eventId;
+ private String eventType;
+ private String timestamp;
+ private UUID transactionExternalId;
+ private UUID accountExternalIdDebit;
+ private UUID accountExternalIdCredit;
+ private Integer transferTypeId;
+ private BigDecimal value;
+}
+
diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/event/TransactionStatusUpdatedEvent.java b/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/event/TransactionStatusUpdatedEvent.java
new file mode 100644
index 0000000000..88c1c55521
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/event/TransactionStatusUpdatedEvent.java
@@ -0,0 +1,26 @@
+package com.yape.antifraud.infrastructure.messaging.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+/**
+ * Evento publicado cuando se valida una transacción
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TransactionStatusUpdatedEvent {
+ private String eventId;
+ private String eventType;
+ private String timestamp;
+ private UUID correlationId;
+ private UUID transactionExternalId;
+ private String newStatus;
+ private String reason;
+}
+
diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/producer/FraudResultProducer.java b/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/producer/FraudResultProducer.java
new file mode 100644
index 0000000000..6cdf600204
--- /dev/null
+++ b/anti-fraud-service/src/main/java/com/yape/antifraud/infrastructure/messaging/producer/FraudResultProducer.java
@@ -0,0 +1,46 @@
+package com.yape.antifraud.infrastructure.messaging.producer;
+
+import com.yape.antifraud.infrastructure.messaging.event.TransactionStatusUpdatedEvent;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.kafka.support.SendResult;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Producer de Kafka para publicar resultados de validación
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class FraudResultProducer {
+
+ private final KafkaTemplate kafkaTemplate;
+
+ @Value("${kafka.topics.transaction-status-updated}")
+ private String transactionStatusUpdatedTopic;
+
+ public void publishValidationResult(TransactionStatusUpdatedEvent event) {
+ log.info("Publishing validation result for transaction: {}", event.getTransactionExternalId());
+
+ CompletableFuture> future = kafkaTemplate.send(
+ transactionStatusUpdatedTopic,
+ event.getTransactionExternalId().toString(),
+ event
+ );
+
+ future.whenComplete((result, ex) -> {
+ if (ex == null) {
+ log.info("Validation result published successfully for transaction: {} with status: {}",
+ event.getTransactionExternalId(), event.getNewStatus());
+ } else {
+ log.error("Failed to publish validation result for transaction: {}",
+ event.getTransactionExternalId(), ex);
+ }
+ });
+ }
+}
+
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..216d04eeb9
--- /dev/null
+++ b/anti-fraud-service/src/main/resources/application.yml
@@ -0,0 +1,55 @@
+spring:
+ application:
+ name: anti-fraud-service
+
+ kafka:
+ bootstrap-servers: localhost:9092
+ consumer:
+ group-id: anti-fraud-service-group
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
+ auto-offset-reset: earliest
+ enable-auto-commit: false
+ properties:
+ spring.json.trusted.packages: '*'
+ spring.json.type.mapping: transaction-created:com.yape.antifraud.infrastructure.messaging.event.TransactionCreatedEvent
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
+ acks: all
+ retries: 3
+ properties:
+ enable.idempotence: true
+ spring.json.add.type.headers: false
+ listener:
+ ack-mode: manual
+
+server:
+ port: 8081
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: health,info,metrics
+ endpoint:
+ health:
+ show-details: always
+
+logging:
+ level:
+ com.yape: INFO
+ org.springframework.kafka: INFO
+ pattern:
+ console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
+
+# Kafka Topics Configuration
+kafka:
+ topics:
+ transaction-created: transaction-created
+ transaction-status-updated: transaction-status-updated
+
+# Anti-Fraud Business Rules
+anti-fraud:
+ max-transaction-value: 1000
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 0e8807f21c..3b4e4fd276 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -2,17 +2,28 @@ version: "3.7"
services:
postgres:
image: postgres:14
+ container_name: yape-postgres
ports:
- "5432:5432"
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
+ - POSTGRES_DB=transactiondb
+ volumes:
+ - postgres-data:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
zookeeper:
image: confluentinc/cp-zookeeper:5.5.3
+ container_name: yape-zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
kafka:
image: confluentinc/cp-enterprise-kafka:5.5.3
+ container_name: yape-kafka
depends_on: [zookeeper]
environment:
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
@@ -23,3 +34,11 @@ services:
KAFKA_JMX_PORT: 9991
ports:
- 9092:9092
+ healthcheck:
+ test: ["CMD", "kafka-broker-api-versions", "--bootstrap-server", "localhost:9092"]
+ interval: 10s
+ timeout: 10s
+ retries: 5
+
+volumes:
+ postgres-data:
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000000..724316a6c5
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,110 @@
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.2.1
+
+
+
+ com.yape
+ transaction-system
+ 1.0.0
+ pom
+
+ Yape Transaction System
+ Sistema de transacciones con validación anti-fraude
+
+
+ transaction-service
+ anti-fraud-service
+
+
+
+ 17
+ 17
+ 17
+ UTF-8
+ 3.2.1
+ 1.18.30
+ 1.5.5.Final
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ ${spring-boot.version}
+ pom
+ import
+
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.projectlombok
+ lombok
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ 17
+ 17
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+ org.projectlombok
+ lombok-mapstruct-binding
+ 0.2.0
+
+
+
+
+
+
+
+
+
diff --git a/transaction-service/pom.xml b/transaction-service/pom.xml
new file mode 100644
index 0000000000..7f4dd46051
--- /dev/null
+++ b/transaction-service/pom.xml
@@ -0,0 +1,107 @@
+
+
+ 4.0.0
+
+
+ com.yape
+ transaction-system
+ 1.0.0
+
+
+ transaction-service
+ Transaction Service
+ Servicio de gestión de transacciones
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+ org.flywaydb
+ flyway-core
+
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+ org.mapstruct
+ mapstruct
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.springframework.kafka
+ spring-kafka-test
+ test
+
+
+ org.testcontainers
+ testcontainers
+ 1.19.3
+ test
+
+
+ org.testcontainers
+ postgresql
+ 1.19.3
+ test
+
+
+ org.testcontainers
+ kafka
+ 1.19.3
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/TransactionServiceApplication.java b/transaction-service/src/main/java/com/yape/transaction/TransactionServiceApplication.java
new file mode 100644
index 0000000000..43cb70d70f
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/TransactionServiceApplication.java
@@ -0,0 +1,18 @@
+package com.yape.transaction;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.kafka.annotation.EnableKafka;
+
+/**
+ * Aplicación principal - Transaction Service
+ */
+@EnableKafka
+@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/transaction/application/dto/CreateTransactionCommand.java b/transaction-service/src/main/java/com/yape/transaction/application/dto/CreateTransactionCommand.java
new file mode 100644
index 0000000000..bb4a66f93f
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/application/dto/CreateTransactionCommand.java
@@ -0,0 +1,24 @@
+package com.yape.transaction.application.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+/**
+ * Comando para crear una transacción
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateTransactionCommand {
+ private UUID accountExternalIdDebit;
+ private UUID accountExternalIdCredit;
+ private Integer tranferTypeId;
+ private BigDecimal value;
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/application/dto/TransactionResponse.java b/transaction-service/src/main/java/com/yape/transaction/application/dto/TransactionResponse.java
new file mode 100644
index 0000000000..769f1b95bc
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/application/dto/TransactionResponse.java
@@ -0,0 +1,42 @@
+package com.yape.transaction.application.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * DTO de respuesta de transacción
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TransactionResponse {
+ private UUID transactionExternalId;
+ private TransactionTypeDTO transactionType;
+ private TransactionStatusDTO transactionStatus;
+ private BigDecimal value;
+ private LocalDateTime createdAt;
+
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class TransactionTypeDTO {
+ private String name;
+ }
+
+ @Data
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class TransactionStatusDTO {
+ private String name;
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/application/service/TransactionService.java b/transaction-service/src/main/java/com/yape/transaction/application/service/TransactionService.java
new file mode 100644
index 0000000000..3abbdc964a
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/application/service/TransactionService.java
@@ -0,0 +1,146 @@
+package com.yape.transaction.application.service;
+
+import com.yape.transaction.application.dto.CreateTransactionCommand;
+import com.yape.transaction.application.dto.TransactionResponse;
+import com.yape.transaction.domain.exception.InvalidTransactionException;
+import com.yape.transaction.domain.exception.TransactionNotFoundException;
+import com.yape.transaction.domain.model.Transaction;
+import com.yape.transaction.domain.model.TransactionType;
+import com.yape.transaction.domain.port.EventPublisher;
+import com.yape.transaction.domain.port.TransactionRepository;
+import com.yape.transaction.infrastructure.messaging.event.TransactionCreatedEvent;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+/**
+ * Servicio de aplicación para gestión de transacciones (Use Cases)
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TransactionService {
+
+ private final TransactionRepository transactionRepository;
+ private final EventPublisher eventPublisher;
+
+ @Value("${kafka.topics.transaction-created}")
+ private String transactionCreatedTopic;
+
+ /**
+ * Caso de uso: Crear transacción
+ */
+ @Transactional
+ public TransactionResponse createTransaction(CreateTransactionCommand command) {
+ log.info("Creating transaction for debit account: {}", command.getAccountExternalIdDebit());
+
+ // Validaciones de negocio
+ validateTransactionCommand(command);
+
+ // Crear transacción en estado PENDING
+ TransactionType transactionType = TransactionType.fromId(command.getTranferTypeId());
+ Transaction transaction = Transaction.createPending(
+ command.getAccountExternalIdDebit(),
+ command.getAccountExternalIdCredit(),
+ transactionType,
+ command.getValue()
+ );
+
+ // Persistir transacción
+ Transaction savedTransaction = transactionRepository.save(transaction);
+ log.info("Transaction created with ID: {}", savedTransaction.getTransactionExternalId());
+
+ // Publicar evento a Kafka
+ publishTransactionCreatedEvent(savedTransaction);
+
+ // Retornar respuesta
+ return mapToResponse(savedTransaction);
+ }
+
+ /**
+ * Caso de uso: Obtener transacción por ID externo
+ */
+ @Transactional(readOnly = true)
+ public TransactionResponse getTransaction(UUID transactionExternalId) {
+ log.info("Getting transaction: {}", transactionExternalId);
+
+ Transaction transaction = transactionRepository.findByExternalId(transactionExternalId)
+ .orElseThrow(() -> new TransactionNotFoundException(transactionExternalId));
+
+ return mapToResponse(transaction);
+ }
+
+ /**
+ * Actualiza el estado de una transacción (llamado por el consumer de Kafka)
+ */
+ @Transactional
+ public void updateTransactionStatus(UUID transactionExternalId, String newStatus) {
+ log.info("Updating transaction {} status to: {}", transactionExternalId, newStatus);
+
+ Transaction transaction = transactionRepository.findByExternalId(transactionExternalId)
+ .orElseThrow(() -> new TransactionNotFoundException(transactionExternalId));
+
+ // Actualizar estado
+ transaction.updateStatus(com.yape.transaction.domain.model.TransactionStatus.fromName(newStatus));
+
+ // Persistir cambios
+ transactionRepository.update(transaction);
+ log.info("Transaction {} updated to status: {}", transactionExternalId, newStatus);
+ }
+
+ private void validateTransactionCommand(CreateTransactionCommand command) {
+ if (command.getAccountExternalIdDebit() == null) {
+ throw new InvalidTransactionException("Debit account ID cannot be null");
+ }
+ if (command.getAccountExternalIdCredit() == null) {
+ throw new InvalidTransactionException("Credit account ID cannot be null");
+ }
+ if (command.getValue() == null || command.getValue().compareTo(BigDecimal.ZERO) <= 0) {
+ throw new InvalidTransactionException("Transaction value must be greater than zero");
+ }
+ if (command.getTranferTypeId() == null) {
+ throw new InvalidTransactionException("Transfer type ID cannot be null");
+ }
+ }
+
+ private void publishTransactionCreatedEvent(Transaction transaction) {
+ TransactionCreatedEvent event = TransactionCreatedEvent.builder()
+ .eventId(UUID.randomUUID().toString())
+ .eventType("TRANSACTION_CREATED")
+ .timestamp(java.time.Instant.now().toString())
+ .transactionExternalId(transaction.getTransactionExternalId())
+ .accountExternalIdDebit(transaction.getAccountExternalIdDebit())
+ .accountExternalIdCredit(transaction.getAccountExternalIdCredit())
+ .transferTypeId(transaction.getTransactionType().getId())
+ .value(transaction.getValue())
+ .build();
+
+ eventPublisher.publish(
+ transactionCreatedTopic,
+ transaction.getTransactionExternalId().toString(),
+ event
+ );
+
+ log.info("Published TransactionCreatedEvent for transaction: {}", transaction.getTransactionExternalId());
+ }
+
+ private TransactionResponse mapToResponse(Transaction transaction) {
+ return TransactionResponse.builder()
+ .transactionExternalId(transaction.getTransactionExternalId())
+ .transactionType(TransactionResponse.TransactionTypeDTO.builder()
+ .name(transaction.getTransactionType().getName())
+ .build())
+ .transactionStatus(TransactionResponse.TransactionStatusDTO.builder()
+ .name(transaction.getTransactionStatus().getName())
+ .build())
+ .value(transaction.getValue())
+ .createdAt(transaction.getCreatedAt())
+ .build();
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/domain/exception/DomainException.java b/transaction-service/src/main/java/com/yape/transaction/domain/exception/DomainException.java
new file mode 100644
index 0000000000..184d853513
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/domain/exception/DomainException.java
@@ -0,0 +1,16 @@
+package com.yape.transaction.domain.exception;
+
+/**
+ * Excepción base de dominio
+ */
+public class DomainException extends RuntimeException {
+
+ public DomainException(String message) {
+ super(message);
+ }
+
+ public DomainException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/domain/exception/InvalidTransactionException.java b/transaction-service/src/main/java/com/yape/transaction/domain/exception/InvalidTransactionException.java
new file mode 100644
index 0000000000..3b607bda5a
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/domain/exception/InvalidTransactionException.java
@@ -0,0 +1,12 @@
+package com.yape.transaction.domain.exception;
+
+/**
+ * Excepción cuando los datos de la transacción son inválidos
+ */
+public class InvalidTransactionException extends DomainException {
+
+ public InvalidTransactionException(String message) {
+ super(message);
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/domain/exception/TransactionNotFoundException.java b/transaction-service/src/main/java/com/yape/transaction/domain/exception/TransactionNotFoundException.java
new file mode 100644
index 0000000000..4dd92ad08c
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/domain/exception/TransactionNotFoundException.java
@@ -0,0 +1,14 @@
+package com.yape.transaction.domain.exception;
+
+import java.util.UUID;
+
+/**
+ * Excepción cuando no se encuentra una transacción
+ */
+public class TransactionNotFoundException extends DomainException {
+
+ public TransactionNotFoundException(UUID transactionExternalId) {
+ super("Transaction not found with id: " + transactionExternalId);
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/domain/model/Transaction.java b/transaction-service/src/main/java/com/yape/transaction/domain/model/Transaction.java
new file mode 100644
index 0000000000..508d64c8a0
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/domain/model/Transaction.java
@@ -0,0 +1,83 @@
+package com.yape.transaction.domain.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * Entidad de dominio Transaction - Modelo rico con lógica de negocio
+ */
+@Getter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Transaction {
+
+ private Long id;
+ private UUID transactionExternalId;
+ private UUID accountExternalIdDebit;
+ private UUID accountExternalIdCredit;
+ private TransactionType transactionType;
+ private TransactionStatus transactionStatus;
+ private BigDecimal value;
+ private LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+ private Long version;
+
+ /**
+ * Crea una nueva transacción en estado PENDING
+ */
+ public static Transaction createPending(
+ UUID accountExternalIdDebit,
+ UUID accountExternalIdCredit,
+ TransactionType transactionType,
+ BigDecimal value) {
+
+ return Transaction.builder()
+ .transactionExternalId(UUID.randomUUID())
+ .accountExternalIdDebit(accountExternalIdDebit)
+ .accountExternalIdCredit(accountExternalIdCredit)
+ .transactionType(transactionType)
+ .transactionStatus(TransactionStatus.PENDING)
+ .value(value)
+ .createdAt(LocalDateTime.now())
+ .updatedAt(LocalDateTime.now())
+ .version(0L)
+ .build();
+ }
+
+ /**
+ * Actualiza el estado de la transacción
+ */
+ public void updateStatus(TransactionStatus newStatus) {
+ this.transactionStatus = newStatus;
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ /**
+ * Verifica si la transacción está pendiente
+ */
+ public boolean isPending() {
+ return TransactionStatus.PENDING.equals(this.transactionStatus);
+ }
+
+ /**
+ * Verifica si la transacción está aprobada
+ */
+ public boolean isApproved() {
+ return TransactionStatus.APPROVED.equals(this.transactionStatus);
+ }
+
+ /**
+ * Verifica si la transacción está rechazada
+ */
+ public boolean isRejected() {
+ return TransactionStatus.REJECTED.equals(this.transactionStatus);
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/domain/model/TransactionStatus.java b/transaction-service/src/main/java/com/yape/transaction/domain/model/TransactionStatus.java
new file mode 100644
index 0000000000..d202287f93
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/domain/model/TransactionStatus.java
@@ -0,0 +1,27 @@
+package com.yape.transaction.domain.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * Enum de estados de transacción
+ */
+@Getter
+@AllArgsConstructor
+public enum TransactionStatus {
+ PENDING("pending"),
+ APPROVED("approved"),
+ REJECTED("rejected");
+
+ private final String name;
+
+ public static TransactionStatus fromName(String name) {
+ for (TransactionStatus status : values()) {
+ if (status.name.equalsIgnoreCase(name)) {
+ return status;
+ }
+ }
+ throw new IllegalArgumentException("Invalid transaction status: " + name);
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/domain/model/TransactionType.java b/transaction-service/src/main/java/com/yape/transaction/domain/model/TransactionType.java
new file mode 100644
index 0000000000..0f0148a2a3
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/domain/model/TransactionType.java
@@ -0,0 +1,37 @@
+package com.yape.transaction.domain.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * Enum de tipos de transacción
+ */
+@Getter
+@AllArgsConstructor
+public enum TransactionType {
+ TRANSFER(1, "transfer"),
+ DEPOSIT(2, "deposit"),
+ WITHDRAWAL(3, "withdrawal");
+
+ private final int id;
+ private final String name;
+
+ public static TransactionType fromId(int id) {
+ for (TransactionType type : values()) {
+ if (type.id == id) {
+ return type;
+ }
+ }
+ throw new IllegalArgumentException("Invalid transaction type id: " + id);
+ }
+
+ public static TransactionType fromName(String name) {
+ for (TransactionType type : values()) {
+ if (type.name.equalsIgnoreCase(name)) {
+ return type;
+ }
+ }
+ throw new IllegalArgumentException("Invalid transaction type: " + name);
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/domain/port/EventPublisher.java b/transaction-service/src/main/java/com/yape/transaction/domain/port/EventPublisher.java
new file mode 100644
index 0000000000..c1585f08a6
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/domain/port/EventPublisher.java
@@ -0,0 +1,13 @@
+package com.yape.transaction.domain.port;
+
+/**
+ * Port de salida para publicación de eventos (Hexagonal Architecture)
+ */
+public interface EventPublisher {
+
+ /**
+ * Publica un evento en el sistema de mensajería
+ */
+ void publish(String topic, String key, T event);
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/domain/port/TransactionRepository.java b/transaction-service/src/main/java/com/yape/transaction/domain/port/TransactionRepository.java
new file mode 100644
index 0000000000..a54ec4b6cf
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/domain/port/TransactionRepository.java
@@ -0,0 +1,28 @@
+package com.yape.transaction.domain.port;
+
+import com.yape.transaction.domain.model.Transaction;
+
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Port de salida para persistencia de transacciones (Hexagonal Architecture)
+ */
+public interface TransactionRepository {
+
+ /**
+ * Guarda una transacción
+ */
+ Transaction save(Transaction transaction);
+
+ /**
+ * Busca una transacción por su ID externo
+ */
+ Optional findByExternalId(UUID transactionExternalId);
+
+ /**
+ * Actualiza una transacción existente
+ */
+ Transaction update(Transaction transaction);
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/controller/TransactionController.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/controller/TransactionController.java
new file mode 100644
index 0000000000..d006aeee6f
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/controller/TransactionController.java
@@ -0,0 +1,62 @@
+package com.yape.transaction.infrastructure.api.controller;
+
+import com.yape.transaction.application.dto.CreateTransactionCommand;
+import com.yape.transaction.application.dto.TransactionResponse;
+import com.yape.transaction.application.service.TransactionService;
+import com.yape.transaction.infrastructure.api.dto.CreateTransactionRequest;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.UUID;
+
+/**
+ * REST Controller para gestión de transacciones
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/transactions")
+@RequiredArgsConstructor
+public class TransactionController {
+
+ private final TransactionService transactionService;
+
+ /**
+ * Endpoint para crear una transacción
+ */
+ @PostMapping
+ public ResponseEntity createTransaction(
+ @Valid @RequestBody CreateTransactionRequest request) {
+
+ log.info("POST /api/v1/transactions - Creating transaction");
+
+ CreateTransactionCommand command = CreateTransactionCommand.builder()
+ .accountExternalIdDebit(request.getAccountExternalIdDebit())
+ .accountExternalIdCredit(request.getAccountExternalIdCredit())
+ .tranferTypeId(request.getTranferTypeId())
+ .value(request.getValue())
+ .build();
+
+ TransactionResponse response = transactionService.createTransaction(command);
+
+ return ResponseEntity.status(HttpStatus.CREATED).body(response);
+ }
+
+ /**
+ * Endpoint para obtener una transacción por ID
+ */
+ @GetMapping("/{transactionExternalId}")
+ public ResponseEntity getTransaction(
+ @PathVariable UUID transactionExternalId) {
+
+ log.info("GET /api/v1/transactions/{} - Getting transaction", transactionExternalId);
+
+ TransactionResponse response = transactionService.getTransaction(transactionExternalId);
+
+ return ResponseEntity.ok(response);
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/dto/CreateTransactionRequest.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/dto/CreateTransactionRequest.java
new file mode 100644
index 0000000000..62b819fec7
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/dto/CreateTransactionRequest.java
@@ -0,0 +1,35 @@
+package com.yape.transaction.infrastructure.api.dto;
+
+import jakarta.validation.constraints.DecimalMin;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+/**
+ * DTO de request para crear transacción
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class CreateTransactionRequest {
+
+ @NotNull(message = "accountExternalIdDebit is required")
+ private UUID accountExternalIdDebit;
+
+ @NotNull(message = "accountExternalIdCredit is required")
+ private UUID accountExternalIdCredit;
+
+ @NotNull(message = "tranferTypeId is required")
+ private Integer tranferTypeId;
+
+ @NotNull(message = "value is required")
+ @DecimalMin(value = "0.01", message = "value must be greater than zero")
+ private BigDecimal value;
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/dto/ErrorResponse.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/dto/ErrorResponse.java
new file mode 100644
index 0000000000..a1e990cb89
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/dto/ErrorResponse.java
@@ -0,0 +1,20 @@
+package com.yape.transaction.infrastructure.api.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * DTO de respuesta de error
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ErrorResponse {
+ private String error;
+ private String message;
+ private String timestamp;
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/exception/GlobalExceptionHandler.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000000..1dcd66107f
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/api/exception/GlobalExceptionHandler.java
@@ -0,0 +1,80 @@
+package com.yape.transaction.infrastructure.api.exception;
+
+import com.yape.transaction.domain.exception.InvalidTransactionException;
+import com.yape.transaction.domain.exception.TransactionNotFoundException;
+import com.yape.transaction.infrastructure.api.dto.ErrorResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.FieldError;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+import java.time.Instant;
+import java.util.stream.Collectors;
+
+/**
+ * Manejador global de excepciones
+ */
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(TransactionNotFoundException.class)
+ public ResponseEntity handleTransactionNotFound(TransactionNotFoundException ex) {
+ log.error("Transaction not found: {}", ex.getMessage());
+
+ ErrorResponse error = ErrorResponse.builder()
+ .error("Transaction not found")
+ .message(ex.getMessage())
+ .timestamp(Instant.now().toString())
+ .build();
+
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
+ }
+
+ @ExceptionHandler(InvalidTransactionException.class)
+ public ResponseEntity handleInvalidTransaction(InvalidTransactionException ex) {
+ log.error("Invalid transaction: {}", ex.getMessage());
+
+ ErrorResponse error = ErrorResponse.builder()
+ .error("Invalid transaction")
+ .message(ex.getMessage())
+ .timestamp(Instant.now().toString())
+ .build();
+
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
+ }
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+ public ResponseEntity handleValidationException(MethodArgumentNotValidException ex) {
+ String errors = ex.getBindingResult().getFieldErrors().stream()
+ .map(FieldError::getDefaultMessage)
+ .collect(Collectors.joining(", "));
+
+ log.error("Validation error: {}", errors);
+
+ ErrorResponse error = ErrorResponse.builder()
+ .error("Validation error")
+ .message(errors)
+ .timestamp(Instant.now().toString())
+ .build();
+
+ return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity handleGenericException(Exception ex) {
+ log.error("Unexpected error", ex);
+
+ ErrorResponse error = ErrorResponse.builder()
+ .error("Internal server error")
+ .message("An unexpected error occurred")
+ .timestamp(Instant.now().toString())
+ .build();
+
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/config/KafkaConfig.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/config/KafkaConfig.java
new file mode 100644
index 0000000000..c687738419
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/config/KafkaConfig.java
@@ -0,0 +1,77 @@
+package com.yape.transaction.infrastructure.messaging.config;
+
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.common.serialization.StringDeserializer;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.annotation.EnableKafka;
+import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
+import org.springframework.kafka.core.*;
+import org.springframework.kafka.listener.ContainerProperties;
+import org.springframework.kafka.support.serializer.JsonDeserializer;
+import org.springframework.kafka.support.serializer.JsonSerializer;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Configuración de Kafka
+ */
+@EnableKafka
+@Configuration
+public class KafkaConfig {
+
+ @Value("${spring.kafka.bootstrap-servers}")
+ private String bootstrapServers;
+
+ @Value("${spring.kafka.consumer.group-id}")
+ private String groupId;
+
+ // Producer Configuration
+ @Bean
+ public ProducerFactory producerFactory() {
+ Map config = new HashMap<>();
+ config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
+ config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
+ config.put(ProducerConfig.ACKS_CONFIG, "all");
+ config.put(ProducerConfig.RETRIES_CONFIG, 3);
+ config.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
+ config.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false);
+ return new DefaultKafkaProducerFactory<>(config);
+ }
+
+ @Bean
+ public KafkaTemplate kafkaTemplate() {
+ return new KafkaTemplate<>(producerFactory());
+ }
+
+ // Consumer Configuration
+ @Bean
+ public ConsumerFactory consumerFactory() {
+ Map config = new HashMap<>();
+ config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ config.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
+ config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+ config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
+ config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
+ config.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false);
+ config.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
+ config.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false);
+ config.put(JsonDeserializer.VALUE_DEFAULT_TYPE, "com.yape.transaction.infrastructure.messaging.event.TransactionStatusUpdatedEvent");
+ return new DefaultKafkaConsumerFactory<>(config);
+ }
+
+ @Bean
+ public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() {
+ ConcurrentKafkaListenerContainerFactory factory =
+ new ConcurrentKafkaListenerContainerFactory<>();
+ factory.setConsumerFactory(consumerFactory());
+ factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL);
+ return factory;
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/consumer/TransactionStatusConsumer.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/consumer/TransactionStatusConsumer.java
new file mode 100644
index 0000000000..4928658df9
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/consumer/TransactionStatusConsumer.java
@@ -0,0 +1,47 @@
+package com.yape.transaction.infrastructure.messaging.consumer;
+
+import com.yape.transaction.application.service.TransactionService;
+import com.yape.transaction.infrastructure.messaging.event.TransactionStatusUpdatedEvent;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.annotation.KafkaListener;
+import org.springframework.kafka.support.Acknowledgment;
+import org.springframework.stereotype.Component;
+
+/**
+ * Consumer de Kafka para eventos de actualización de estado
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TransactionStatusConsumer {
+
+ private final TransactionService transactionService;
+
+ @KafkaListener(
+ topics = "${kafka.topics.transaction-status-updated}",
+ groupId = "${spring.kafka.consumer.group-id}",
+ containerFactory = "kafkaListenerContainerFactory"
+ )
+ public void consume(TransactionStatusUpdatedEvent event, Acknowledgment acknowledgment) {
+ try {
+ log.info("Received TransactionStatusUpdatedEvent: {}", event);
+
+ // Actualizar estado de la transacción
+ transactionService.updateTransactionStatus(
+ event.getTransactionExternalId(),
+ event.getNewStatus()
+ );
+
+ // Confirmar mensaje procesado
+ acknowledgment.acknowledge();
+ log.info("TransactionStatusUpdatedEvent processed successfully");
+
+ } catch (Exception e) {
+ log.error("Error processing TransactionStatusUpdatedEvent: {}", event, e);
+ // En un escenario real, aquí implementarías retry logic o DLQ
+ acknowledgment.acknowledge(); // Por ahora, confirmamos para evitar loops infinitos
+ }
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/event/TransactionCreatedEvent.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/event/TransactionCreatedEvent.java
new file mode 100644
index 0000000000..cc4bf8e33f
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/event/TransactionCreatedEvent.java
@@ -0,0 +1,28 @@
+package com.yape.transaction.infrastructure.messaging.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+/**
+ * Evento publicado cuando se crea una transacción
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TransactionCreatedEvent {
+ private String eventId;
+ private String eventType;
+ private String timestamp;
+ private UUID transactionExternalId;
+ private UUID accountExternalIdDebit;
+ private UUID accountExternalIdCredit;
+ private Integer transferTypeId;
+ private BigDecimal value;
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/event/TransactionStatusUpdatedEvent.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/event/TransactionStatusUpdatedEvent.java
new file mode 100644
index 0000000000..47883e6d2d
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/event/TransactionStatusUpdatedEvent.java
@@ -0,0 +1,26 @@
+package com.yape.transaction.infrastructure.messaging.event;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+/**
+ * Evento recibido cuando se actualiza el estado de una transacción
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TransactionStatusUpdatedEvent {
+ private String eventId;
+ private String eventType;
+ private String timestamp;
+ private UUID correlationId; // transactionExternalId
+ private UUID transactionExternalId;
+ private String newStatus;
+ private String reason;
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/producer/KafkaEventPublisher.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/producer/KafkaEventPublisher.java
new file mode 100644
index 0000000000..ebd8b51168
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/messaging/producer/KafkaEventPublisher.java
@@ -0,0 +1,38 @@
+package com.yape.transaction.infrastructure.messaging.producer;
+
+import com.yape.transaction.domain.port.EventPublisher;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.kafka.core.KafkaTemplate;
+import org.springframework.kafka.support.SendResult;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.CompletableFuture;
+
+/**
+ * Adaptador de Kafka Producer - Implementa el port EventPublisher
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class KafkaEventPublisher implements EventPublisher {
+
+ private final KafkaTemplate kafkaTemplate;
+
+ @Override
+ public void publish(String topic, String key, T event) {
+ log.info("Publishing event to topic: {} with key: {}", topic, key);
+
+ CompletableFuture> future = kafkaTemplate.send(topic, key, event);
+
+ future.whenComplete((result, ex) -> {
+ if (ex == null) {
+ log.info("Event published successfully to topic: {} with offset: {}",
+ topic, result.getRecordMetadata().offset());
+ } else {
+ log.error("Failed to publish event to topic: {}", topic, ex);
+ }
+ });
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/persistence/adapter/TransactionPersistenceAdapter.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/persistence/adapter/TransactionPersistenceAdapter.java
new file mode 100644
index 0000000000..bbd882b42f
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/persistence/adapter/TransactionPersistenceAdapter.java
@@ -0,0 +1,93 @@
+package com.yape.transaction.infrastructure.persistence.adapter;
+
+import com.yape.transaction.domain.model.Transaction;
+import com.yape.transaction.domain.model.TransactionStatus;
+import com.yape.transaction.domain.model.TransactionType;
+import com.yape.transaction.domain.port.TransactionRepository;
+import com.yape.transaction.infrastructure.persistence.entity.TransactionEntity;
+import com.yape.transaction.infrastructure.persistence.repository.JpaTransactionRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Adaptador de persistencia - Implementa el port TransactionRepository
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TransactionPersistenceAdapter implements TransactionRepository {
+
+ private final JpaTransactionRepository jpaRepository;
+
+ @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 Transaction update(Transaction transaction) {
+ TransactionEntity entity = toEntity(transaction);
+ TransactionEntity updatedEntity = jpaRepository.save(entity);
+ return toDomain(updatedEntity);
+ }
+
+ private TransactionEntity toEntity(Transaction transaction) {
+ return TransactionEntity.builder()
+ .id(transaction.getId())
+ .transactionExternalId(transaction.getTransactionExternalId())
+ .accountExternalIdDebit(transaction.getAccountExternalIdDebit())
+ .accountExternalIdCredit(transaction.getAccountExternalIdCredit())
+ .transferTypeId((long) transaction.getTransactionType().getId())
+ .transactionStatusId(mapStatusToId(transaction.getTransactionStatus()))
+ .value(transaction.getValue())
+ .version(transaction.getVersion())
+ .createdAt(transaction.getCreatedAt())
+ .updatedAt(transaction.getUpdatedAt())
+ .build();
+ }
+
+ private Transaction toDomain(TransactionEntity entity) {
+ return Transaction.builder()
+ .id(entity.getId())
+ .transactionExternalId(entity.getTransactionExternalId())
+ .accountExternalIdDebit(entity.getAccountExternalIdDebit())
+ .accountExternalIdCredit(entity.getAccountExternalIdCredit())
+ .transactionType(TransactionType.fromId(entity.getTransferTypeId().intValue()))
+ .transactionStatus(mapIdToStatus(entity.getTransactionStatusId()))
+ .value(entity.getValue())
+ .version(entity.getVersion())
+ .createdAt(entity.getCreatedAt())
+ .updatedAt(entity.getUpdatedAt())
+ .build();
+ }
+
+ private Long mapStatusToId(TransactionStatus status) {
+ return switch (status) {
+ case PENDING -> 1L;
+ case APPROVED -> 2L;
+ case REJECTED -> 3L;
+ };
+ }
+
+ private TransactionStatus mapIdToStatus(Long id) {
+ return switch (id.intValue()) {
+ case 1 -> TransactionStatus.PENDING;
+ case 2 -> TransactionStatus.APPROVED;
+ case 3 -> TransactionStatus.REJECTED;
+ default -> throw new IllegalArgumentException("Invalid status id: " + id);
+ };
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/persistence/entity/TransactionEntity.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/persistence/entity/TransactionEntity.java
new file mode 100644
index 0000000000..6ca59b3e46
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/persistence/entity/TransactionEntity.java
@@ -0,0 +1,74 @@
+package com.yape.transaction.infrastructure.persistence.entity;
+
+import jakarta.persistence.*;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+/**
+ * Entidad JPA para transacciones
+ */
+@Entity
+@Table(name = "transaction")
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class TransactionEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(name = "transaction_external_id", nullable = false, unique = true)
+ private UUID transactionExternalId;
+
+ @Column(name = "account_external_id_debit", nullable = false)
+ private UUID accountExternalIdDebit;
+
+ @Column(name = "account_external_id_credit", nullable = false)
+ private UUID accountExternalIdCredit;
+
+ @Column(name = "transfer_type_id", nullable = false)
+ private Long transferTypeId;
+
+ @Column(name = "transaction_status_id", nullable = false)
+ private Long transactionStatusId;
+
+ @Column(name = "value", nullable = false, precision = 19, scale = 2)
+ private BigDecimal value;
+
+ @Version
+ @Column(name = "version")
+ private Long version;
+
+ @Column(name = "created_at", nullable = false, updatable = false)
+ private LocalDateTime createdAt;
+
+ @Column(name = "updated_at", nullable = false)
+ private LocalDateTime updatedAt;
+
+ @PrePersist
+ protected void onCreate() {
+ if (transactionExternalId == null) {
+ transactionExternalId = UUID.randomUUID();
+ }
+ if (createdAt == null) {
+ createdAt = LocalDateTime.now();
+ }
+ if (updatedAt == null) {
+ updatedAt = LocalDateTime.now();
+ }
+ }
+
+ @PreUpdate
+ protected void onUpdate() {
+ updatedAt = LocalDateTime.now();
+ }
+}
+
diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/persistence/repository/JpaTransactionRepository.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/persistence/repository/JpaTransactionRepository.java
new file mode 100644
index 0000000000..52e08b3967
--- /dev/null
+++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/persistence/repository/JpaTransactionRepository.java
@@ -0,0 +1,18 @@
+package com.yape.transaction.infrastructure.persistence.repository;
+
+import com.yape.transaction.infrastructure.persistence.entity.TransactionEntity;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+import java.util.UUID;
+
+/**
+ * Repositorio JPA para transacciones
+ */
+@Repository
+public interface JpaTransactionRepository extends JpaRepository {
+
+ Optional findByTransactionExternalId(UUID transactionExternalId);
+}
+
diff --git a/transaction-service/src/main/resources/application.yml b/transaction-service/src/main/resources/application.yml
new file mode 100644
index 0000000000..494cc4bb73
--- /dev/null
+++ b/transaction-service/src/main/resources/application.yml
@@ -0,0 +1,78 @@
+spring:
+ application:
+ name: transaction-service
+
+ datasource:
+ url: jdbc:postgresql://localhost:5432/transactiondb
+ username: postgres
+ password: postgres
+ driver-class-name: org.postgresql.Driver
+ hikari:
+ maximum-pool-size: 20
+ minimum-idle: 10
+ connection-timeout: 30000
+ idle-timeout: 600000
+ max-lifetime: 1800000
+
+ jpa:
+ hibernate:
+ ddl-auto: validate
+ show-sql: false
+ properties:
+ hibernate:
+ format_sql: true
+ dialect: org.hibernate.dialect.PostgreSQLDialect
+
+ flyway:
+ enabled: true
+ baseline-on-migrate: true
+ locations: classpath:db/migration
+
+ kafka:
+ bootstrap-servers: localhost:9092
+ producer:
+ key-serializer: org.apache.kafka.common.serialization.StringSerializer
+ value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
+ acks: all
+ retries: 3
+ properties:
+ enable.idempotence: true
+ spring.json.add.type.headers: false
+ consumer:
+ group-id: transaction-service-group
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer
+ auto-offset-reset: earliest
+ enable-auto-commit: false
+ properties:
+ spring.json.trusted.packages: '*'
+ spring.json.type.mapping: transaction-status-updated:com.yape.transaction.infrastructure.messaging.event.TransactionStatusUpdatedEvent
+ listener:
+ ack-mode: manual
+
+server:
+ port: 8080
+
+management:
+ endpoints:
+ web:
+ exposure:
+ include: health,info,metrics
+ endpoint:
+ health:
+ show-details: always
+
+logging:
+ level:
+ com.yape: INFO
+ org.springframework.kafka: INFO
+ org.hibernate.SQL: DEBUG
+ pattern:
+ console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
+
+# Kafka Topics Configuration
+kafka:
+ topics:
+ transaction-created: transaction-created
+ transaction-status-updated: transaction-status-updated
+
diff --git a/transaction-service/src/main/resources/db/migration/V1__create_transaction_tables.sql b/transaction-service/src/main/resources/db/migration/V1__create_transaction_tables.sql
new file mode 100644
index 0000000000..c510b0b53e
--- /dev/null
+++ b/transaction-service/src/main/resources/db/migration/V1__create_transaction_tables.sql
@@ -0,0 +1,50 @@
+-- Tabla de tipos de transacción
+CREATE TABLE transaction_type (
+ id BIGSERIAL PRIMARY KEY,
+ name VARCHAR(50) NOT NULL UNIQUE,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Tabla de estados de transacción
+CREATE TABLE transaction_status (
+ id BIGSERIAL PRIMARY KEY,
+ name VARCHAR(20) NOT NULL UNIQUE
+ CHECK (name IN ('pending', 'approved', 'rejected')),
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Tabla principal de transacciones
+CREATE TABLE transaction (
+ id BIGSERIAL PRIMARY KEY,
+ transaction_external_id UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(),
+ account_external_id_debit UUID NOT NULL,
+ account_external_id_credit UUID NOT NULL,
+ transfer_type_id BIGINT NOT NULL REFERENCES transaction_type(id),
+ transaction_status_id BIGINT NOT NULL REFERENCES transaction_status(id),
+ value DECIMAL(19,2) NOT NULL CHECK (value > 0),
+ version BIGINT DEFAULT 0,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Índices para optimizar búsquedas
+CREATE INDEX idx_transaction_external_id ON transaction(transaction_external_id);
+CREATE INDEX idx_transaction_status ON transaction(transaction_status_id);
+CREATE INDEX idx_transaction_created_at ON transaction(created_at DESC);
+CREATE INDEX idx_transaction_debit_account ON transaction(account_external_id_debit);
+
+-- Función para actualizar updated_at automáticamente
+CREATE OR REPLACE FUNCTION update_updated_at_column()
+RETURNS TRIGGER AS $$
+BEGIN
+ NEW.updated_at = CURRENT_TIMESTAMP;
+ RETURN NEW;
+END;
+$$ language 'plpgsql';
+
+-- Trigger para actualizar updated_at en cada UPDATE
+CREATE TRIGGER update_transaction_updated_at
+ BEFORE UPDATE ON transaction
+ FOR EACH ROW
+ EXECUTE FUNCTION update_updated_at_column();
+
diff --git a/transaction-service/src/main/resources/db/migration/V2__insert_initial_data.sql b/transaction-service/src/main/resources/db/migration/V2__insert_initial_data.sql
new file mode 100644
index 0000000000..2a870f98d8
--- /dev/null
+++ b/transaction-service/src/main/resources/db/migration/V2__insert_initial_data.sql
@@ -0,0 +1,12 @@
+-- Insertar estados de transacción
+INSERT INTO transaction_status (name) VALUES
+ ('pending'),
+ ('approved'),
+ ('rejected');
+
+-- Insertar tipos de transacción
+INSERT INTO transaction_type (name) VALUES
+ ('transfer'),
+ ('deposit'),
+ ('withdrawal');
+