diff --git a/business-transaction-anti-fraud-v1/Dockerfile b/business-transaction-anti-fraud-v1/Dockerfile
new file mode 100644
index 0000000..6b54c40
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/Dockerfile
@@ -0,0 +1,21 @@
+# Use an official OpenJDK runtime as a parent image
+FROM openjdk:17-jdk-slim
+
+# Set the working directory in the container
+WORKDIR /app
+
+# Copy the Maven project file and the source code
+# COPY pom.xml /app
+# COPY src /app/src
+
+# Package the application
+# RUN ./mvnw clean install -DskipTests
+
+# Copy the packaged JAR file to the working directory
+COPY target/*.jar app.jar
+
+# Exponer el puerto en el que la aplicación se ejecutará (cambiar si es necesario)
+EXPOSE 8080
+
+# Run the application
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/business-transaction-anti-fraud-v1/README.md b/business-transaction-anti-fraud-v1/README.md
new file mode 100644
index 0000000..5060acd
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/README.md
@@ -0,0 +1,42 @@
+# API business-transaction-anti-fraud
+
+## Input Payload
+
+```json
+{
+ "id": "1",
+ "amount": "100",
+ "currency": "USD"
+}
+```
+
+## Output Payload
+
+```json
+{
+ "transactionId": "1",
+ "isFraudulent": false,
+ "reason": ""
+}
+```
+
+
+# ingress-nginx
+
+**ingress-nginx** es un controlador de Ingress para Kubernetes basado en NGINX. Se utiliza para gestionar el acceso externo a los servicios en un clúster de Kubernetes, proporcionando balanceo de carga, terminación SSL y enrutamiento basado en host y ruta.
+
+## Características principales
+
+- **Balanceo de carga**: Distribuye el tráfico entrante entre múltiples réplicas de un servicio.
+- **Terminación SSL**: Maneja la terminación SSL/TLS para asegurar las conexiones.
+- **Enrutamiento basado en host y ruta**: Permite definir reglas para enrutar el tráfico a diferentes servicios basados en el host y la ruta de la solicitud.
+
+## Instalación
+
+Para instalar **ingress-nginx** en un clúster de Kubernetes, puedes usar Helm:
+
+```bash
+helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
+helm repo update
+helm install ingress-nginx ingress-nginx/ingress-nginx --namespace ingress-nginx --create-namespace
+
diff --git a/business-transaction-anti-fraud-v1/checkstyle-checks.xml b/business-transaction-anti-fraud-v1/checkstyle-checks.xml
new file mode 100644
index 0000000..aef1686
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/checkstyle-checks.xml
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/business-transaction-anti-fraud-v1/checkstyle-suppressions.xml b/business-transaction-anti-fraud-v1/checkstyle-suppressions.xml
new file mode 100644
index 0000000..35cfde6
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/checkstyle-suppressions.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/business-transaction-anti-fraud-v1/copyright_header.txt b/business-transaction-anti-fraud-v1/copyright_header.txt
new file mode 100644
index 0000000..e69de29
diff --git a/business-transaction-anti-fraud-v1/docker-compose.yml b/business-transaction-anti-fraud-v1/docker-compose.yml
new file mode 100644
index 0000000..e3f7a98
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/docker-compose.yml
@@ -0,0 +1,35 @@
+version: '3'
+services:
+ zookeeper:
+ image: confluentinc/cp-zookeeper:7.0.1
+ container_name: zookeeper
+ environment:
+ ZOOKEEPER_CLIENT_PORT: 2181
+ ZOOKEEPER_TICK_TIME: 2000
+ ports:
+ - "2181:2181"
+
+ kafka:
+ image: confluentinc/cp-kafka:7.0.1
+ container_name: kafka
+ depends_on:
+ - zookeeper
+ ports:
+ - "9092:9092"
+ environment:
+ KAFKA_BROKER_ID: 1
+ KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092
+ KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ volumes:
+ - ./kafka-data:/var/lib/kafka/data
+
+ kafka-manager:
+ image: hlebalbau/kafka-manager:latest
+ container_name: kafka-manager
+ depends_on:
+ - kafka
+ ports:
+ - "9000:9000"
+ environment:
+ ZK_HOSTS: zookeeper:2181
\ No newline at end of file
diff --git a/business-transaction-anti-fraud-v1/kubectl/deployment.yaml b/business-transaction-anti-fraud-v1/kubectl/deployment.yaml
new file mode 100644
index 0000000..a104726
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/kubectl/deployment.yaml
@@ -0,0 +1,27 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: business-transaction-anti-fraud-v1
+spec:
+ replicas: 1 # Número inicial de réplicas
+ selector:
+ matchLabels:
+ app: business-transaction-anti-fraud-v1
+ template:
+ metadata:
+ labels:
+ app: business-transaction-anti-fraud-v1
+ spec:
+ containers:
+ - name: business-transaction-anti-fraud-v1-container
+ image: vllave/business-transaction-anti-fraud-v1:latest
+ resources:
+ limits:
+ cpu: "2" # cpu_limits
+ memory: "1512Mi" # memory_limits
+ requests:
+ cpu: "40m" # cpu_requests
+ memory: "256Mi" # memory_requests
+ env:
+ - name: JAVA_OPTS
+ value: "-Xms32m -Xmx1024m" # jvm_xms y jvm_xmx
\ No newline at end of file
diff --git a/business-transaction-anti-fraud-v1/kubectl/hpa.yaml b/business-transaction-anti-fraud-v1/kubectl/hpa.yaml
new file mode 100644
index 0000000..98fe5ee
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/kubectl/hpa.yaml
@@ -0,0 +1,24 @@
+apiVersion: autoscaling/v2
+kind: HorizontalPodAutoscaler
+metadata:
+ name: business-transaction-anti-fraud-v1-hpa
+spec:
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: business-transaction-anti-fraud-v1
+ minReplicas: 1
+ maxReplicas: 4
+ metrics:
+ - type: Resource
+ resource:
+ name: cpu
+ target:
+ type: Utilization
+ averageUtilization: 900 # target_average_cpu (representa el % de uso de CPU por pod)
+ - type: Resource
+ resource:
+ name: memory
+ target:
+ type: Utilization
+ averageUtilization: 350 # target_average_memory (representa el % de uso de memoria por pod)
\ No newline at end of file
diff --git a/business-transaction-anti-fraud-v1/kubectl/ingress.yaml b/business-transaction-anti-fraud-v1/kubectl/ingress.yaml
new file mode 100644
index 0000000..887bf1f
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/kubectl/ingress.yaml
@@ -0,0 +1,19 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: business-transaction-anti-fraud-v1-ingress
+ annotations:
+ nginx.ingress.kubernetes.io/rewrite-target: /
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: localhost
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: business-transaction-anti-fraud-v1-service
+ port:
+ number: 80
\ No newline at end of file
diff --git a/business-transaction-anti-fraud-v1/kubectl/service.yaml b/business-transaction-anti-fraud-v1/kubectl/service.yaml
new file mode 100644
index 0000000..32ab383
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/kubectl/service.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: business-transaction-anti-fraud-v1-service
+spec:
+ selector:
+ app: business-transaction-anti-fraud-v1
+ ports:
+ - protocol: TCP
+ port: 80 # Puerto interno del servicio
+ targetPort: 8085 # Puerto en el contenedor del pod
+ nodePort: 30007 # Puerto expuesto en el nodo
+# type: LoadBalancer
+ type: NodePort # Usar NodePort para exponer el servicio localmente
diff --git a/business-transaction-anti-fraud-v1/pom.xml b/business-transaction-anti-fraud-v1/pom.xml
new file mode 100644
index 0000000..fb1ccb5
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/pom.xml
@@ -0,0 +1,185 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.2
+
+
+ com.bcp.services.trxantifraud
+ business-transaction-anti-fraud
+ 1.0.0
+ business-transaction-anti-fraud
+ business transaction anti fraud
+
+
+ 17
+ 4.7.3.4
+ 1.12.0
+ 1.5.0
+ false
+
+
+
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+ io.projectreactor.kafka
+ reactor-kafka
+
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.openapitools
+ jackson-databind-nullable
+ 0.2.6
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.16.0
+
+
+
+ org.mockito
+ mockito-core
+ 5.12.0
+ test
+
+
+
+ io.projectreactor
+ reactor-test
+ 3.6.9
+ test
+
+
+
+ io.projectreactor.addons
+ reactor-adapter
+ 3.5.2
+
+
+
+ com.github.spotbugs
+ spotbugs-annotations
+ 4.7.3
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.4.2
+
+
+
+ com.bcp.services.trxantifraud.StartApplication
+
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${spotbugs-maven-plugin.version}
+
+
+ com.github.spotbugs
+ spotbugs
+ 4.7.3
+
+
+ com.github.spotbugs
+ spotbugs-annotations
+ 4.7.3
+
+
+
+ Max
+ Low
+ true
+ spotbugs-exclude.xml
+
+
+ com.h3xstream.findsecbugs
+ findsecbugs-plugin
+ ${findsecbugs-plugin.version}
+
+
+ jp.skypencil.findbugs.slf4j
+ bug-pattern
+ ${bug-pattern-plugin.version}
+
+
+
+ true
+
+
+ spotbugs-check
+ verify
+
+ check
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.3.0
+
+ checkstyle-checks.xml
+ copyright_header.txt
+ checkstyle-suppressions.xml
+ true
+ true
+ false
+
+
+
+ validate
+ validate
+
+ check
+
+
+
+
+
+
+
+
diff --git a/business-transaction-anti-fraud-v1/spotbugs-exclude.xml b/business-transaction-anti-fraud-v1/spotbugs-exclude.xml
new file mode 100644
index 0000000..1600e28
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/spotbugs-exclude.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/StartApplication.java b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/StartApplication.java
new file mode 100644
index 0000000..c0eb5d2
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/StartApplication.java
@@ -0,0 +1,16 @@
+package com.bcp.services.trxantifraud;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * StartApplication.
+ * This class is used to start the application.
+ */
+@SpringBootApplication
+public class StartApplication {
+
+ public static void main(final String[] args) {
+ SpringApplication.run(StartApplication.class, args);
+ }
+}
diff --git a/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/ReactiveKafkaConsumer.java b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/ReactiveKafkaConsumer.java
new file mode 100644
index 0000000..792ee02
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/ReactiveKafkaConsumer.java
@@ -0,0 +1,96 @@
+package com.bcp.services.trxantifraud.business;
+
+import com.bcp.services.trxantifraud.model.AnalysisResult;
+import com.bcp.services.trxantifraud.model.RawTransaction;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+import reactor.kafka.receiver.KafkaReceiver;
+import reactor.kafka.receiver.ReceiverRecord;
+
+import java.util.UUID;
+
+/**
+ * Reactive Kafka Consumer.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ReactiveKafkaConsumer {
+
+ @Value("${application.kafka.producer.topic}")
+ private String topic;
+
+ private final KafkaReceiver kafkaReceiver;
+ private final TransactionService transactionService;
+ private final ReactiveKafkaProducer reactiveKafkaProducer;
+ private final ObjectMapper objectMapper;
+
+ @PostConstruct
+ public void initialize() {
+ startConsuming();
+ }
+
+ private void startConsuming() {
+
+ kafkaReceiver.receive()
+ .doOnNext(this::processMessage)
+ .flatMap(this::processRecord)
+ .subscribe();
+ }
+
+ private void processMessage(final ConsumerRecord record) {
+ log.info("Message received: key = {}, value = {}", record.key(), record.value());
+ }
+
+ private Mono processRecord(final ReceiverRecord record) {
+ return Mono.just(record)
+ .map(this::getRawTransaction)
+ .flatMap(this::analyzeRawTransaction)
+ .flatMap(this::sendAnalysisResult)
+ .doOnError(error -> log.error("Error processing record: ", error))
+ .then(acknowledgeRecord(record));
+ }
+
+ private RawTransaction getRawTransaction(final ReceiverRecord record) {
+ try {
+ return objectMapper.readValue(record.value(), RawTransaction.class);
+ } catch (JsonProcessingException ex) {
+ log.error("Error parsing message: ", ex);
+ throw new RuntimeException(ex);
+ }
+ }
+
+ private Mono analyzeRawTransaction(final RawTransaction transaction) {
+ return transactionService.analyzeTrx(Mono.just(transaction))
+ .doOnError(error -> log.error("Error analyzing transaction: ", error));
+ }
+
+ private Mono sendAnalysisResult(final AnalysisResult analysisResult) {
+ String analysisResultJson = getResultJson(analysisResult);
+ return reactiveKafkaProducer.sendMessage(topic, UUID.randomUUID().toString(), analysisResultJson)
+ .doOnError(error -> log.error("Error sending message: ", error));
+ }
+
+ private Mono acknowledgeRecord(final ReceiverRecord record) {
+ return Mono.fromRunnable(record.receiverOffset()::acknowledge)
+ .doOnError(error -> log.error("Error acknowledging message: ", error))
+ .then();
+ }
+
+ private String getResultJson(final AnalysisResult analysisResult) {
+ try {
+ return objectMapper.writeValueAsString(analysisResult);
+ } catch (JsonProcessingException ex) {
+ log.error("Error parsing message: ", ex);
+ throw new RuntimeException(ex);
+ }
+ }
+
+}
diff --git a/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/ReactiveKafkaProducer.java b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/ReactiveKafkaProducer.java
new file mode 100644
index 0000000..20fe921
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/ReactiveKafkaProducer.java
@@ -0,0 +1,29 @@
+package com.bcp.services.trxantifraud.business;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+import reactor.kafka.sender.KafkaSender;
+import reactor.kafka.sender.SenderRecord;
+
+/**
+ * Reactive Kafka Producer.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ReactiveKafkaProducer {
+
+ private final KafkaSender kafkaSender;
+
+ public Mono sendMessage(final String topic, final String key, final String value) {
+ ProducerRecord record = new ProducerRecord<>(topic, key, value);
+ SenderRecord senderRecord = SenderRecord.create(record, key);
+
+ return kafkaSender.send(Mono.just(senderRecord))
+ .doOnError(throwable -> log.error("Error sending message: {}", throwable.getMessage()))
+ .then();
+ }
+}
diff --git a/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/TransactionService.java b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/TransactionService.java
new file mode 100644
index 0000000..10b786d
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/TransactionService.java
@@ -0,0 +1,15 @@
+package com.bcp.services.trxantifraud.business;
+
+import com.bcp.services.trxantifraud.model.AnalysisResult;
+import com.bcp.services.trxantifraud.model.RawTransaction;
+import reactor.core.publisher.Mono;
+
+/**
+ * Transaction Service.
+ * This interface is used to represent a TransactionService.
+ */
+public interface TransactionService {
+
+ Mono analyzeTrx(Mono transaction);
+
+}
diff --git a/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/impl/TransactionServiceImpl.java b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/impl/TransactionServiceImpl.java
new file mode 100644
index 0000000..cf047d5
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/business/impl/TransactionServiceImpl.java
@@ -0,0 +1,45 @@
+package com.bcp.services.trxantifraud.business.impl;
+
+import com.bcp.services.trxantifraud.business.TransactionService;
+import com.bcp.services.trxantifraud.config.ApplicationProperties;
+import com.bcp.services.trxantifraud.model.AnalysisResult;
+import com.bcp.services.trxantifraud.model.RawTransaction;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+/**
+ * Transaction Service Implementation.
+ */
+@Service
+@RequiredArgsConstructor
+public class TransactionServiceImpl implements TransactionService {
+
+ public static final String REASON = "Transaction amount exceeds the maximum allowed amount";
+ private final ApplicationProperties applicationProperties;
+
+ @Override
+ public Mono analyzeTrx(final Mono transaction) {
+
+ return transaction.filter(trx -> trx.getAmount()
+ .compareTo(applicationProperties.getMaximumTransactionAmount()) <= 0)
+ .map(this::createLegitimateResult)
+ .switchIfEmpty(transaction.map(this::createFraudulentResult));
+ }
+
+ private AnalysisResult createLegitimateResult(final RawTransaction trx) {
+ return AnalysisResult.builder()
+ .isFraudulent(false)
+ .transactionId(trx.getId())
+ .build();
+ }
+
+ private AnalysisResult createFraudulentResult(final RawTransaction trx) {
+ return AnalysisResult.builder()
+ .isFraudulent(true)
+ .transactionId(trx.getId())
+ .reason(REASON)
+ .build();
+ }
+
+}
diff --git a/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ApplicationProperties.java b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ApplicationProperties.java
new file mode 100644
index 0000000..2c1a72f
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ApplicationProperties.java
@@ -0,0 +1,20 @@
+package com.bcp.services.trxantifraud.config;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.math.BigDecimal;
+
+/**
+ * Application Properties.
+ */
+@Getter
+@Setter
+@Configuration
+@ConfigurationProperties(prefix = "application")
+public class ApplicationProperties {
+
+ private BigDecimal maximumTransactionAmount;
+}
\ No newline at end of file
diff --git a/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ObjectMapperConfig.java b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ObjectMapperConfig.java
new file mode 100644
index 0000000..b170c02
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ObjectMapperConfig.java
@@ -0,0 +1,18 @@
+package com.bcp.services.trxantifraud.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Configuration class for Jackson ObjectMapper.
+ */
+@Configuration
+public class ObjectMapperConfig {
+
+ @Bean(name = "objectMapper2")
+ public ObjectMapper objectMapper() {
+ return new ObjectMapper();
+ }
+}
+
diff --git a/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ReactiveKafkaConsumerConfig.java b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ReactiveKafkaConsumerConfig.java
new file mode 100644
index 0000000..f3dd3fb
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ReactiveKafkaConsumerConfig.java
@@ -0,0 +1,54 @@
+package com.bcp.services.trxantifraud.config;
+
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import reactor.kafka.receiver.KafkaReceiver;
+import reactor.kafka.receiver.ReceiverOptions;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Reactive Kafka Consumer Configuration.
+ */
+@Configuration
+public class ReactiveKafkaConsumerConfig {
+
+ @Value("${spring.kafka.bootstrap-servers}")
+ private String bootstrapServers;
+
+ @Value("${spring.kafka.consumer.group-id}")
+ private String groupId;
+
+ @Value("${spring.kafka.consumer.auto-offset-reset}")
+ private String autoOffsetReset;
+
+ @Value("${spring.kafka.consumer.enable-auto-commit}")
+ private boolean enableAutoCommit;
+
+ @Value("${spring.kafka.consumer.key-deserializer}")
+ private String keyDeserializer;
+
+ @Value("${spring.kafka.consumer.value-deserializer}")
+ private String valueDeserializer;
+
+ @Value("${application.kafka.consumer.topic}")
+ private String topic;
+
+ @Bean
+ public KafkaReceiver kafkaReceiver() {
+ Map consumerProps = new HashMap<>();
+ consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
+ consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
+ consumerProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, enableAutoCommit);
+ consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, keyDeserializer);
+ consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, valueDeserializer);
+
+ ReceiverOptions receiverOptions = ReceiverOptions.create(consumerProps);
+ return KafkaReceiver.create(receiverOptions.subscription(Collections.singleton(topic)));
+ }
+}
diff --git a/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ReactiveKafkaProducerConfig.java b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ReactiveKafkaProducerConfig.java
new file mode 100644
index 0000000..9877635
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/config/ReactiveKafkaProducerConfig.java
@@ -0,0 +1,38 @@
+package com.bcp.services.trxantifraud.config;
+
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import reactor.kafka.sender.KafkaSender;
+import reactor.kafka.sender.SenderOptions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Reactive Kafka Producer Configuration.
+ */
+@Configuration
+public class ReactiveKafkaProducerConfig {
+
+ @Value("${spring.kafka.bootstrap-servers}")
+ private String bootstrapServers;
+
+ @Value("${spring.kafka.producer.key-serializer}")
+ private String keySerializer;
+
+ @Value("${spring.kafka.producer.value-serializer}")
+ private String valueSerializer;
+
+ @Bean
+ public KafkaSender kafkaSender() {
+ Map producerProps = new HashMap<>();
+ producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, keySerializer);
+ producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, valueSerializer);
+
+ SenderOptions senderOptions = SenderOptions.create(producerProps);
+ return KafkaSender.create(senderOptions);
+ }
+}
diff --git a/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/model/AnalysisResult.java b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/model/AnalysisResult.java
new file mode 100644
index 0000000..d3b1bdd
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/model/AnalysisResult.java
@@ -0,0 +1,20 @@
+package com.bcp.services.trxantifraud.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * AnalysisResult.
+ * This class is used to represent an AnalysisResult.
+ */
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class AnalysisResult {
+ private String transactionId;
+ private Boolean isFraudulent;
+ private String reason;
+}
diff --git a/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/model/RawTransaction.java b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/model/RawTransaction.java
new file mode 100644
index 0000000..4da8b9b
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/java/com/bcp/services/trxantifraud/model/RawTransaction.java
@@ -0,0 +1,22 @@
+package com.bcp.services.trxantifraud.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+/**
+ * RawTransaction.
+ * This class is used to represent a RawTransaction.
+ */
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class RawTransaction {
+ private String id;
+ private BigDecimal amount;
+ private String currency;
+}
\ No newline at end of file
diff --git a/business-transaction-anti-fraud-v1/src/main/resources/application-local.yml b/business-transaction-anti-fraud-v1/src/main/resources/application-local.yml
new file mode 100644
index 0000000..408daa1
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/resources/application-local.yml
@@ -0,0 +1,13 @@
+application:
+ maximum-transaction-amount: 1000
+ kafka:
+ producer:
+ topic: transaction-processing
+ consumer:
+ group-id: reactive-group
+ topic: transaction-anti-fraud-validation
+logging:
+ level:
+ root: INFO
+ com.bcp.services.trxantifraud: INFO
+
diff --git a/business-transaction-anti-fraud-v1/src/main/resources/application.yml b/business-transaction-anti-fraud-v1/src/main/resources/application.yml
new file mode 100644
index 0000000..f4f1760
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/main/resources/application.yml
@@ -0,0 +1,20 @@
+spring:
+ application:
+ name: business-transaction-anti-fraud
+ webflux:
+ base-path: /transaction-anti-fraud/v1
+ 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: reactive-group
+ auto-offset-reset: earliest
+ enable-auto-commit: false
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ profiles:
+ active: local
+server:
+ port: 8080
\ No newline at end of file
diff --git a/business-transaction-anti-fraud-v1/src/test/java/com/bcp/services/trxantifraud/business/ReactiveKafkaConsumerTest.java b/business-transaction-anti-fraud-v1/src/test/java/com/bcp/services/trxantifraud/business/ReactiveKafkaConsumerTest.java
new file mode 100644
index 0000000..f2669b9
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/test/java/com/bcp/services/trxantifraud/business/ReactiveKafkaConsumerTest.java
@@ -0,0 +1,118 @@
+package com.bcp.services.trxantifraud.business;
+
+import com.bcp.services.trxantifraud.model.AnalysisResult;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.TopicPartition;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.junit.jupiter.MockitoExtension;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.kafka.receiver.KafkaReceiver;
+import reactor.kafka.receiver.ReceiverOffset;
+import reactor.kafka.receiver.ReceiverRecord;
+import reactor.test.StepVerifier;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+@ExtendWith(MockitoExtension.class)
+class ReactiveKafkaConsumerTest {
+
+ @Mock
+ private KafkaReceiver kafkaReceiver;
+
+ @Mock
+ private TransactionService transactionService;
+
+ @Mock
+ private ReactiveKafkaProducer reactiveKafkaProducer;
+
+ @Spy
+ private ObjectMapper objectMapper;
+
+ @InjectMocks
+ private ReactiveKafkaConsumer reactiveKafkaConsumer;
+
+ @Test
+ @DisplayName("Initialize successfully")
+ void initializeSuccessfully() {
+
+ ConsumerRecord consumerRecord = new ConsumerRecord<>("test-topic", 0, 0L,
+ "9295f66a-6db4-4a39-9af2-2c524f49fb39",
+ "{\"id\":\"cc34b401-ba74-41e6-9079-5e5b9567d938\",\"amount\":\"200.00\",\"currency\":\"USD\"}");
+
+ ReceiverRecord mockRecord = getStringStringReceiverRecord(consumerRecord);
+ doReturn(Flux.just(mockRecord)).when(kafkaReceiver).receive();
+
+ // Arrange
+ var analysisResult = AnalysisResult.builder().build();
+ doReturn(Mono.empty()).when(reactiveKafkaProducer).sendMessage(any(), any(), any());
+ doReturn(Mono.just(analysisResult)).when(transactionService).analyzeTrx(any());
+
+ // Act
+ reactiveKafkaConsumer.initialize();
+
+ // Assert
+ StepVerifier.create(transactionService.analyzeTrx(any()))
+ .expectNext(analysisResult)
+ .expectComplete()
+ .verify();
+
+ StepVerifier.create(reactiveKafkaProducer.sendMessage(any(), any(), any()))
+ .expectComplete()
+ .verify();
+ }
+
+ @Test
+ @DisplayName("Fail to initialize due to Kafka error")
+ void failToInitializeDueToKafkaError() {
+ // Arrange
+ ConsumerRecord consumerRecord = new ConsumerRecord<>("test-topic", 0, 0L,
+ "9295f66a-6db4-4a39-9af2-2c524f49fb39",
+ "{\"id\":\"cc34b401-ba74-41e6-9079-5e5b9567d938\",\"amount\":\"ee.00\",\"currency\":\"USD\"}");
+
+ ReceiverRecord mockRecord = getStringStringReceiverRecord(consumerRecord);
+ doReturn(Flux.just(mockRecord)).when(kafkaReceiver).receive();
+
+ reactiveKafkaConsumer.initialize();
+
+ // Assert
+ verify(transactionService, never()).analyzeTrx(any());
+ }
+
+ private static ReceiverRecord getStringStringReceiverRecord(
+ ConsumerRecord consumerRecord) {
+ ReceiverOffset receiverOffset = new ReceiverOffset() {
+
+ @Override
+ public TopicPartition topicPartition() {
+ return new TopicPartition("test-topic", 0);
+ }
+
+ @Override
+ public long offset() {
+ return 0;
+ }
+
+ @Override
+ public void acknowledge() {
+
+ }
+
+ @Override
+ public Mono commit() {
+ return null;
+ }
+ };
+
+ return new ReceiverRecord<>(consumerRecord, receiverOffset);
+ }
+}
\ No newline at end of file
diff --git a/business-transaction-anti-fraud-v1/src/test/java/com/bcp/services/trxantifraud/business/ReactiveKafkaProducerTest.java b/business-transaction-anti-fraud-v1/src/test/java/com/bcp/services/trxantifraud/business/ReactiveKafkaProducerTest.java
new file mode 100644
index 0000000..c97acc0
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/test/java/com/bcp/services/trxantifraud/business/ReactiveKafkaProducerTest.java
@@ -0,0 +1,56 @@
+package com.bcp.services.trxantifraud.business;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.kafka.sender.KafkaSender;
+import reactor.test.StepVerifier;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+
+@ExtendWith(MockitoExtension.class)
+class ReactiveKafkaProducerTest {
+
+ @Mock
+ private KafkaSender kafkaSender;
+
+ @InjectMocks
+ private ReactiveKafkaProducer reactiveKafkaProducer;
+
+ @Test
+ @DisplayName("Send message successfully")
+ void sendMessageSuccessfully() {
+ // Arrange
+ doReturn(Flux.empty()).when(kafkaSender).send(any(Mono.class));
+
+ // Act
+ Mono result = reactiveKafkaProducer.sendMessage("test-topic", "test-key", "test-value");
+
+ // Assert
+ StepVerifier.create(result)
+ .expectComplete()
+ .verify();
+ }
+
+ @Test
+ @DisplayName("Fail to send message")
+ void failToSendMessage() {
+
+ // Arrange
+ doReturn(Flux.error(new RuntimeException("Kafka send error"))).when(kafkaSender).send(any(Mono.class));
+
+ // Act
+ Mono result = reactiveKafkaProducer.sendMessage("test-topic", "test-key", "test-value");
+
+ // Assert
+ StepVerifier.create(result)
+ .expectErrorMessage("Kafka send error")
+ .verify();
+ }
+}
\ No newline at end of file
diff --git a/business-transaction-anti-fraud-v1/src/test/java/com/bcp/services/trxantifraud/business/impl/RawTransactionServiceImplTest.java b/business-transaction-anti-fraud-v1/src/test/java/com/bcp/services/trxantifraud/business/impl/RawTransactionServiceImplTest.java
new file mode 100644
index 0000000..4078e6f
--- /dev/null
+++ b/business-transaction-anti-fraud-v1/src/test/java/com/bcp/services/trxantifraud/business/impl/RawTransactionServiceImplTest.java
@@ -0,0 +1,110 @@
+package com.bcp.services.trxantifraud.business.impl;
+
+import com.bcp.services.trxantifraud.config.ApplicationProperties;
+import com.bcp.services.trxantifraud.model.RawTransaction;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class RawTransactionServiceImplTest {
+
+ @Mock
+ private ApplicationProperties applicationProperties;
+
+ @InjectMocks
+ private TransactionServiceImpl transactionService;
+
+ @Test
+ @DisplayName("Return a legitimate transaction when the transaction amount is less than the maximum allowed amount")
+ void returnALegitimateTransactionWhenTheTransactionAmountIsLessThanTheMaximumAllowedAmount() {
+
+ // Arrange
+ when(applicationProperties.getMaximumTransactionAmount()).thenReturn(BigDecimal.valueOf(1000));
+
+ var uuid = UUID.randomUUID().toString();
+ var rawTransaction = Mono.just(RawTransaction.builder()
+ .id(uuid)
+ .amount(BigDecimal.valueOf(500))
+ .build());
+
+ // Act
+ var result = transactionService.analyzeTrx(rawTransaction);
+
+ // Assert
+ StepVerifier.create(result)
+ .assertNext(result1 -> {
+ Assertions.assertNotNull(result1);
+ Assertions.assertEquals(uuid, result1.getTransactionId());
+ Assertions.assertEquals(false, result1.getIsFraudulent());
+ })
+ .expectComplete()
+ .verify();
+ }
+
+ @Test
+ @DisplayName("Return a legitimate transaction when the transaction amount is equal to the maximum allowed amount")
+ void returnALegitimateTransactionWhenTheTransactionAmountIsEqualToTheMaximumAllowedAmount() {
+
+ // Arrange
+ when(applicationProperties.getMaximumTransactionAmount()).thenReturn(BigDecimal.valueOf(1000));
+
+ var uuid = UUID.randomUUID().toString();
+ var rawTransaction = Mono.just(RawTransaction.builder()
+ .id(uuid)
+ .amount(BigDecimal.valueOf(1000))
+ .build());
+
+ // Act
+ var result = transactionService.analyzeTrx(rawTransaction);
+
+ // Assert
+ StepVerifier.create(result)
+ .assertNext(result1 -> {
+ Assertions.assertNotNull(result1);
+ Assertions.assertEquals(uuid, result1.getTransactionId());
+ Assertions.assertEquals(false, result1.getIsFraudulent());
+ })
+ .expectComplete()
+ .verify();
+ }
+
+ @Test
+ @DisplayName("Return a fraudulent transaction when the transaction amount exceeds the maximum allowed amount")
+ void returnAFraudulentTransactionWhenTheTransactionAmountExceedsTheMaximumAllowedAmount() {
+
+ // Arrange
+ when(applicationProperties.getMaximumTransactionAmount()).thenReturn(BigDecimal.valueOf(1000));
+
+ var uuid = UUID.randomUUID().toString();
+ var rawTransaction = Mono.just(RawTransaction.builder()
+ .id(uuid)
+ .amount(BigDecimal.valueOf(1500))
+ .build());
+
+ // Act
+ var result = transactionService.analyzeTrx(rawTransaction);
+
+ // Assert
+ StepVerifier.create(result)
+ .assertNext(result1 -> {
+ Assertions.assertNotNull(result1);
+ Assertions.assertEquals(uuid, result1.getTransactionId());
+ Assertions.assertEquals(true, result1.getIsFraudulent());
+ Assertions.assertEquals("Transaction amount exceeds the maximum allowed amount", result1.getReason());
+ })
+ .expectComplete()
+ .verify();
+ }
+}
\ No newline at end of file
diff --git a/business-transaction-v1/.openapi-generator-ignore b/business-transaction-v1/.openapi-generator-ignore
new file mode 100644
index 0000000..2ca2093
--- /dev/null
+++ b/business-transaction-v1/.openapi-generator-ignore
@@ -0,0 +1,27 @@
+# OpenAPI Generator Ignore
+# Generated by openapi-generator https://github.com/openapitools/openapi-generator
+
+# Use this file to prevent files from being overwritten by the generator.
+# The patterns follow closely to .gitignore or .dockerignore.
+
+# As an example, the C# client generator defines ApiClient.cs.
+# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
+#ApiClient.cs
+
+# You can match any string of characters against a directory, file or extension with a single asterisk (*):
+#foo/*/qux
+# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
+
+# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
+#foo/**/qux
+**/invoker/**
+**/ApiExceptionDetail.java
+**/ModelApiException.java
+# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
+
+# You can also negate patterns with an exclamation (!).
+# For example, you can ignore all files in a docs folder with the file extension .md:
+#docs/*.md
+# Then explicitly reverse the ignore rule for a single file:
+#!docs/README.md
+# Prevent generator from creating these files:
\ No newline at end of file
diff --git a/business-transaction-v1/Dockerfile b/business-transaction-v1/Dockerfile
new file mode 100644
index 0000000..5f21733
--- /dev/null
+++ b/business-transaction-v1/Dockerfile
@@ -0,0 +1,21 @@
+# Use an official OpenJDK runtime as a parent image
+FROM openjdk:17-jdk-slim
+
+# Set the working directory in the container
+WORKDIR /app
+
+# Copy the Maven project file and the source code
+# COPY pom.xml /app
+# COPY src /app/src
+
+# Package the application
+# RUN ./mvnw clean install -DskipTests
+
+# Copy the packaged JAR file to the working directory
+COPY target/business-transaction-1.0.0.jar app.jar
+
+# Exponer el puerto en el que la aplicación se ejecutará (cambiar si es necesario)
+EXPOSE 8085
+
+# Run the application
+ENTRYPOINT ["java", "-jar", "app.jar"]
diff --git a/business-transaction-v1/README.md b/business-transaction-v1/README.md
new file mode 100644
index 0000000..ce5dab7
--- /dev/null
+++ b/business-transaction-v1/README.md
@@ -0,0 +1,194 @@
+# API Business Transaction V1
+
+# Pasos para instalar postgresql en docker
+
+### 1. Ejecutar el siguiente comando para descargar la imagen de postgresql
+```bash
+docker-compose up -d
+```
+# Pasos para instalar kafka en docker
+
+### 1. Ejecutar el siguiente comando para descargar la imagen de kafka
+```bash
+docker-compose up -d
+```
+### 2. Limpia los volúmenes de datos persistentes:
+Elimina los datos persistidos de Kafka. Dependiendo de cómo configuraste tus volúmenes en docker-compose.yml, esto puede implicar borrar la carpeta que contiene los datos de Kafka.
+
+```bash
+docker-compose down
+```
+### 3. Esto eliminará los datos persistidos por Kafka, incluidos los archivos de meta.properties.
+
+```bash
+sudo rm -rf ./kafka-data/*
+```
+
+# Pasos para Crear base datos en postgresql
+
+### 1. Crear la base de datos
+```sql
+CREATE SCHEMA IF NOT EXISTS trx;
+
+
+DROP TABLE IF EXISTS trx.transaction;
+
+
+CREATE TABLE trx.transaction(id varchar(36) PRIMARY KEY,
+ accountExternalIdDebit varchar(40),
+ accountExternalIdCredit varchar(40),
+ transactionTypeCode varchar(20),
+ amount numeric,
+ status varchar(10),
+ createdAt timestamp,
+ updatedAt timestamp);
+
+GRANT ALL PRIVILEGES ON SCHEMA trx TO postgresuser;
+
+GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA trx TO postgresuser;
+```
+
+# Pasos para Crear y Subir una Imagen a Docker Hub
+
+### 1. Crear un `Dockerfile`
+Define cómo se debe construir tu imagen. Ejemplo básico para una aplicación Node.js:
+
+```dockerfile
+# Usar la imagen base oficial de OpenJDK con Java 17
+FROM openjdk:17-jdk-slim
+
+# Establecer el directorio de trabajo dentro del contenedor
+WORKDIR /app
+
+# Copiar el archivo JAR de la aplicación al directorio de trabajo
+COPY target/myapp.jar /app/myapp.jar
+
+# Exponer el puerto en el que la aplicación se ejecutará (cambiar si es necesario)
+EXPOSE 8080
+
+# Comando para ejecutar la aplicación Java
+ENTRYPOINT ["java", "-jar", "myapp.jar"]
+
+```
+
+### 2. Construir la Imagen
+Usa el siguiente comando para construir la imagen:
+
+```bash
+docker build -t /: .
+```
+
+### 3. Iniciar Sesión en Docker Hub
+Inicia sesión en Docker Hub desde la terminal usando el siguiente comando:
+
+```bash
+docker login
+```
+### 4. Subir la Imagen a Docker Hub
+Una vez que hayas iniciado sesión, sube la imagen a Docker Hub usando el siguiente comando:
+
+```bash
+docker push /:
+```
+### 5. Verificar la Imagen
+Verifica que la imagen se haya subido correctamente iniciando sesión en [Docker Hub](https://hub.docker.com/).
+
+1. Inicia sesión en tu cuenta de Docker Hub.
+2. Dirígete a tu perfil o al repositorio donde subiste la imagen.
+3. La imagen debería aparecer con el nombre y la etiqueta que especificaste.
+
+### 6. Descargar y Ejecutar la Imagen
+
+Para descargar y ejecutar la imagen en otro entorno, usa los siguientes comandos:
+
+```bash
+docker pull /:
+docker run -p 4000:8080 -d /:
+```
+
+# Pasos para Ejecutar los Planes de `resources_plan` y `replicas_plan` en Kubernetes
+
+### 1. Crear el Archivo `deployment.yaml`
+Define el archivo `deployment.yaml` con los recursos de CPU, memoria y configuraciones de JVM:
+
+```yaml
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: my-application
+spec:
+ replicas: 1 # Número inicial de réplicas
+ selector:
+ matchLabels:
+ app: my-application
+ template:
+ metadata:
+ labels:
+ app: my-application
+ spec:
+ containers:
+ - name: my-container
+ image: my-image:latest
+ resources:
+ limits:
+ cpu: "2" # cpu_limits
+ memory: "1512Mi" # memory_limits
+ requests:
+ cpu: "40m" # cpu_requests
+ memory: "256Mi" # memory_requests
+ env:
+ - name: JAVA_OPTS
+ value: "-Xms32m -Xmx1024m" # jvm_xms y jvm_xmx
+```
+
+### 2. Crear el Archivo `hpa.yaml`
+Define el escalado automático en un archivo `hpa.yaml`:
+
+```yaml
+apiVersion: autoscaling/v2
+kind: HorizontalPodAutoscaler
+metadata:
+ name: my-application-hpa
+spec:
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: my-application
+ minReplicas: 1
+ maxReplicas: 4
+ metrics:
+ - type: Resource
+ resource:
+ name: cpu
+ target:
+ type: Utilization
+ averageUtilization: 900
+ - type: Resource
+ resource:
+ name: memory
+ target:
+ type: Utilization
+ averageUtilization: 350
+```
+
+### 3. Aplicar los Manifiestos YAML
+Aplica los archivos `deployment.yaml` y `hpa.yaml` al clúster de Kubernetes con los siguientes comandos:
+
+```bash
+kubectl apply -f deployment.yaml
+kubectl apply -f hpa.yaml
+```
+### 4. Verificar el Estado de los Recursos
+Comprueba el estado del Deployment y del Horizontal Pod Autoscaler (HPA) para asegurarte de que todo está funcionando correctamente:
+
+```bash
+kubectl get deployment my-application
+kubectl get hpa my-application-hpa
+```
+
+### 5. Monitorear el Escalado
+Monitorea cómo el Horizontal Pod Autoscaler (HPA) ajusta las réplicas automáticamente basándose en las métricas de CPU y memoria:
+
+```bash
+kubectl describe hpa my-application-hpa
+```
\ No newline at end of file
diff --git a/business-transaction-v1/checkstyle-checks.xml b/business-transaction-v1/checkstyle-checks.xml
new file mode 100644
index 0000000..aef1686
--- /dev/null
+++ b/business-transaction-v1/checkstyle-checks.xml
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/business-transaction-v1/checkstyle-suppressions.xml b/business-transaction-v1/checkstyle-suppressions.xml
new file mode 100644
index 0000000..e6ddc75
--- /dev/null
+++ b/business-transaction-v1/checkstyle-suppressions.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/business-transaction-v1/copyright_header.txt b/business-transaction-v1/copyright_header.txt
new file mode 100644
index 0000000..e69de29
diff --git a/business-transaction-v1/docker-compose.yml b/business-transaction-v1/docker-compose.yml
new file mode 100644
index 0000000..2abf693
--- /dev/null
+++ b/business-transaction-v1/docker-compose.yml
@@ -0,0 +1,16 @@
+version: '3.8'
+
+services:
+ postgres:
+ image: postgres:latest
+ environment:
+ POSTGRES_DB: mypostgresdb
+ POSTGRES_USER: postgresuser
+ POSTGRES_PASSWORD: postgrespassword
+ volumes:
+ - postgres-data:/var/lib/postgresql/data
+ ports:
+ - "5432:5432"
+
+volumes:
+ postgres-data:
\ No newline at end of file
diff --git a/business-transaction-v1/kubectl/deployment.yaml b/business-transaction-v1/kubectl/deployment.yaml
new file mode 100644
index 0000000..04bcbef
--- /dev/null
+++ b/business-transaction-v1/kubectl/deployment.yaml
@@ -0,0 +1,27 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: business-transaction-v1
+spec:
+ replicas: 1 # Número inicial de réplicas
+ selector:
+ matchLabels:
+ app: business-transaction-v1
+ template:
+ metadata:
+ labels:
+ app: business-transaction-v1
+ spec:
+ containers:
+ - name: business-transaction-v1-container
+ image: vllave/business-transaction-v1:latest
+ resources:
+ limits:
+ cpu: "2" # cpu_limits
+ memory: "1512Mi" # memory_limits
+ requests:
+ cpu: "40m" # cpu_requests
+ memory: "256Mi" # memory_requests
+ env:
+ - name: JAVA_OPTS
+ value: "-Xms32m -Xmx1024m" # jvm_xms y jvm_xmx
\ No newline at end of file
diff --git a/business-transaction-v1/kubectl/hpa.yaml b/business-transaction-v1/kubectl/hpa.yaml
new file mode 100644
index 0000000..8c1ef21
--- /dev/null
+++ b/business-transaction-v1/kubectl/hpa.yaml
@@ -0,0 +1,24 @@
+apiVersion: autoscaling/v2
+kind: HorizontalPodAutoscaler
+metadata:
+ name: business-transaction-v1-hpa
+spec:
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: business-transaction-v1
+ minReplicas: 1
+ maxReplicas: 4
+ metrics:
+ - type: Resource
+ resource:
+ name: cpu
+ target:
+ type: Utilization
+ averageUtilization: 900 # target_average_cpu (representa el % de uso de CPU por pod)
+ - type: Resource
+ resource:
+ name: memory
+ target:
+ type: Utilization
+ averageUtilization: 350 # target_average_memory (representa el % de uso de memoria por pod)
\ No newline at end of file
diff --git a/business-transaction-v1/kubectl/ingress.yaml b/business-transaction-v1/kubectl/ingress.yaml
new file mode 100644
index 0000000..e61f30f
--- /dev/null
+++ b/business-transaction-v1/kubectl/ingress.yaml
@@ -0,0 +1,19 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: business-transaction-v1-ingress
+ annotations:
+ nginx.ingress.kubernetes.io/rewrite-target: /
+spec:
+ ingressClassName: nginx
+ rules:
+ - host: localhost
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: business-transaction-v1-service
+ port:
+ number: 80
\ No newline at end of file
diff --git a/business-transaction-v1/kubectl/service.yaml b/business-transaction-v1/kubectl/service.yaml
new file mode 100644
index 0000000..2e24e42
--- /dev/null
+++ b/business-transaction-v1/kubectl/service.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: business-transaction-v1-service
+spec:
+ selector:
+ app: business-transaction-v1
+ ports:
+ - protocol: TCP
+ port: 80
+ targetPort: 8085
+ type: LoadBalancer
\ No newline at end of file
diff --git a/business-transaction-v1/pom.xml b/business-transaction-v1/pom.xml
new file mode 100644
index 0000000..95d6bc0
--- /dev/null
+++ b/business-transaction-v1/pom.xml
@@ -0,0 +1,360 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.2
+
+
+ com.bcp.services.transaction
+ business-transaction
+ 1.0.0
+ business-transaction
+ business transaction
+
+
+
+ 17
+
+ 4.7.3.4
+ 1.12.0
+ 1.5.0
+
+ false
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+ io.projectreactor.kafka
+ reactor-kafka
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+ 3.0.2
+
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+ 6.0.0
+ provided
+
+
+
+ org.projectlombok
+ lombok
+ true
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ org.openapitools
+ jackson-databind-nullable
+ 0.2.6
+
+
+
+ com.google.guava
+ guava
+ 33.1.0-jre
+
+
+
+ io.swagger.core.v3
+ swagger-annotations
+ 2.2.22
+
+
+
+ org.apache.commons
+ commons-lang3
+ 3.16.0
+
+
+
+ commons-io
+ commons-io
+ 2.16.1
+
+
+
+ io.gsonfire
+ gson-fire
+ 1.8.5
+
+
+
+ org.mockito
+ mockito-core
+ 5.3.1
+ test
+
+
+
+ io.projectreactor
+ reactor-test
+ 3.6.9
+ test
+
+
+
+ io.projectreactor.addons
+ reactor-adapter
+ 3.5.2
+
+
+
+ com.github.spotbugs
+ spotbugs-annotations
+ 4.7.3
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-r2dbc
+
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+ org.postgresql
+ r2dbc-postgresql
+ runtime
+
+
+
+ io.vavr
+ vavr
+ 0.10.4
+
+
+
+ org.skyscreamer
+ jsonassert
+ 1.5.3
+ test
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+
+
+
+ org.springdoc
+ springdoc-openapi-webflux-ui
+ 1.8.0
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.4.2
+
+
+
+ com.bcp.services.transaction.StartApplication
+
+
+
+
+
+ org.openapitools
+ openapi-generator-maven-plugin
+ 7.7.0
+
+
+ transaction
+
+ generate
+
+
+ ${project.basedir}/src/main/resources/openapi.yaml
+ spring
+ spring-boot
+ ${project.groupId}.trx.api
+ ${project.groupId}.trx.model
+
+ ApiUtil.java
+
+
+ ./.openapi-generator-ignore
+
+
+ true
+ true
+ true
+ false
+ false
+
+ true
+ true
+ true
+ true
+ true
+ true
+ false
+ true
+ true
+
+
+
+
+ OffsetDateTime=LocalDateTime
+ ApiException=ApiException
+ ApiExceptionDetail=ApiExceptionDetail
+
+
+ java.time.OffsetDateTime=java.time.LocalDateTime
+ LocalDateTime=java.time.LocalDateTime
+ ApiException=com.bcp.services.transaction.config.core.utils.exception.ApiException
+ ApiExceptionDetail=com.bcp.services.transaction.config.core.utils.exception.ApiExceptionDetail
+
+
+
+
+
+
+ com.google.code.maven-replacer-plugin
+ replacer
+ 1.5.3
+
+
+ generate-sources
+
+ replace
+
+
+
+
+ ${project.basedir}/target/
+
+ **/TransactionApi.java
+ **/TransactionApiDelegate.java
+
+
+
+ import com.bcp.services.transaction.invoker.CollectionFormats.*;
+
+
+
+ LocalDateTime requestDate
+ java.time.LocalDateTime requestDate
+
+
+
+
+
+
+
+ com.github.spotbugs
+ spotbugs-maven-plugin
+ ${spotbugs-maven-plugin.version}
+
+
+ com.github.spotbugs
+ spotbugs
+ 4.7.3
+
+
+ com.github.spotbugs
+ spotbugs-annotations
+ 4.7.3
+
+
+
+ Max
+ Low
+ true
+ spotbugs-exclude.xml
+
+
+ com.h3xstream.findsecbugs
+ findsecbugs-plugin
+ ${findsecbugs-plugin.version}
+
+
+ jp.skypencil.findbugs.slf4j
+ bug-pattern
+ ${bug-pattern-plugin.version}
+
+
+
+ true
+
+
+ spotbugs-check
+ verify
+
+ check
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-checkstyle-plugin
+ 3.3.0
+
+ checkstyle-checks.xml
+ copyright_header.txt
+ checkstyle-suppressions.xml
+ true
+ true
+ false
+
+
+
+ validate
+ validate
+
+ check
+
+
+
+
+
+
+
+
diff --git a/business-transaction-v1/spotbugs-exclude.xml b/business-transaction-v1/spotbugs-exclude.xml
new file mode 100644
index 0000000..ca798da
--- /dev/null
+++ b/business-transaction-v1/spotbugs-exclude.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/StartApplication.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/StartApplication.java
new file mode 100644
index 0000000..0e1faf9
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/StartApplication.java
@@ -0,0 +1,16 @@
+package com.bcp.services.transaction;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * StartApplication.
+ * This class is used to start the application.
+ */
+@SpringBootApplication
+public class StartApplication {
+
+ public static void main(final String[] args) {
+ SpringApplication.run(StartApplication.class, args);
+ }
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/ReactiveKafkaConsumer.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/ReactiveKafkaConsumer.java
new file mode 100644
index 0000000..ffc459a
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/ReactiveKafkaConsumer.java
@@ -0,0 +1,50 @@
+package com.bcp.services.transaction.business;
+
+import com.bcp.services.transaction.model.AnalysisResult;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import jakarta.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+import reactor.kafka.receiver.KafkaReceiver;
+
+/**
+ * Reactive Kafka Consumer.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ReactiveKafkaConsumer {
+
+ private final KafkaReceiver kafkaReceiver;
+ private final TransactionService transactionService;
+ private final ObjectMapper objectMapper;
+
+ @PostConstruct
+ public void initialize() {
+ startConsuming();
+ }
+
+ private void startConsuming() {
+
+ kafkaReceiver.receive()
+ .doOnNext(this::processMessage)
+ .flatMap(record -> Mono.fromCallable(() -> objectMapper.readValue(record.value(), AnalysisResult.class))
+ .flatMap(transactionService::updateTrx)
+ .doOnError(error -> log.error("[0] Error processing analysis result: ", error))
+ .then(Mono.fromRunnable(record.receiverOffset()::acknowledge))
+ .onErrorResume(ex -> {
+ log.error("[1] Error processing analysis result: ", ex);
+ return Mono.empty();
+ }))
+ .subscribe();
+
+ }
+
+ private void processMessage(final ConsumerRecord record) {
+ log.info("Message received: key = {}, value = {}", record.key(), record.value());
+ }
+
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/ReactiveKafkaProducer.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/ReactiveKafkaProducer.java
new file mode 100644
index 0000000..1875564
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/ReactiveKafkaProducer.java
@@ -0,0 +1,33 @@
+package com.bcp.services.transaction.business;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+import reactor.kafka.sender.KafkaSender;
+import reactor.kafka.sender.SenderRecord;
+
+/**
+ * Reactive Kafka Producer.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ReactiveKafkaProducer {
+
+ private final KafkaSender kafkaSender;
+
+ public Mono sendMessage(final String topic, final String key, final String value) {
+ ProducerRecord record = new ProducerRecord<>(topic, key, value);
+ SenderRecord senderRecord = SenderRecord.create(record, key);
+
+ return kafkaSender.send(Mono.just(senderRecord))
+ .flatMap(result -> Mono.empty())
+ .onErrorResume(throwable -> {
+ log.error("Error sending message: {}", throwable.getMessage());
+ return Mono.empty();
+ })
+ .then();
+ }
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/TransactionService.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/TransactionService.java
new file mode 100644
index 0000000..f40acc6
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/TransactionService.java
@@ -0,0 +1,19 @@
+package com.bcp.services.transaction.business;
+
+import com.bcp.services.transaction.model.AnalysisResult;
+import com.bcp.services.transaction.trx.model.GetTransactionResponse;
+import com.bcp.services.transaction.trx.model.TransactionRequest;
+import com.bcp.services.transaction.trx.model.TransactionResponse;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+/**
+ * Transaction Service.
+ */
+public interface TransactionService {
+
+ Mono createTrx(Mono transaction);
+ Mono getTrx(UUID transactionExternalId);
+ Mono updateTrx(AnalysisResult transaction);
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/impl/TransactionServiceImpl.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/impl/TransactionServiceImpl.java
new file mode 100644
index 0000000..e7c6f45
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/business/impl/TransactionServiceImpl.java
@@ -0,0 +1,158 @@
+package com.bcp.services.transaction.business.impl;
+
+import com.bcp.services.transaction.business.ReactiveKafkaProducer;
+import com.bcp.services.transaction.business.TransactionService;
+import com.bcp.services.transaction.config.ApplicationProperties;
+import com.bcp.services.transaction.model.AnalysisResult;
+import com.bcp.services.transaction.model.RawTransaction;
+import com.bcp.services.transaction.model.TransactionEntity;
+import com.bcp.services.transaction.repository.TransactionRepository;
+import com.bcp.services.transaction.trx.model.GetTransactionResponse;
+import com.bcp.services.transaction.trx.model.TransactionRequest;
+import com.bcp.services.transaction.trx.model.TransactionResponse;
+import com.bcp.services.transaction.trx.model.TransactionStatusResponse;
+import com.bcp.services.transaction.trx.model.TransactionTypeResponse;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import static com.bcp.services.transaction.util.exception.CustomApiException.C4091;
+import static com.bcp.services.transaction.util.exception.CustomApiException.C5001;
+import static com.bcp.services.transaction.util.exception.CustomApiException.C5003;
+import static com.bcp.services.transaction.util.exception.ExceptionUtils.buildApiExceptionFromPostgresqlThrowable;
+
+/**
+ * Transaction Service Implementation.
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TransactionServiceImpl implements TransactionService {
+
+ @Value("${application.kafka.producer.topic}")
+ private String topic;
+
+ private final TransactionRepository transactionRepository;
+ private final R2dbcEntityTemplate entityTemplate;
+ private final ReactiveKafkaProducer reactiveKafkaProducer;
+ private final ApplicationProperties applicationProperties;
+ private final ObjectMapper objectMapper;
+
+ @Override
+ public Mono createTrx(final Mono requestMono) {
+
+ return requestMono
+ .map(this::buildTransactionEntity)
+ .flatMap(this::saveTransaction)
+ .flatMap(transactionEntity -> sendMessage(transactionEntity)
+ .then(Mono.just(transactionEntity)))
+ .map(this::buildTransactionResponse);
+ }
+
+ private TransactionEntity buildTransactionEntity(final TransactionRequest request) {
+ return TransactionEntity.builder()
+ .id(UUID.randomUUID().toString())
+ .accountExternalIdDebit(request.getAccountExternalIdDebit().toString())
+ .accountExternalIdCredit(request.getAccountExternalIdCredit().toString())
+ .transactionTypeCode(request.getTransactionTypeCode())
+ .amount(request.getAmount())
+ .status(applicationProperties.getTrxStatuses().getPending())
+ .createdAt(LocalDateTime.now())
+ .build();
+ }
+
+ private Mono saveTransaction(final TransactionEntity transactionEntity) {
+ return entityTemplate
+ .insert(TransactionEntity.class)
+ .using(transactionEntity)
+ .doOnError((Throwable ex) -> log.error("Error saving transaction", ex))
+ .onErrorResume((Throwable ex) -> Mono.error(buildApiExceptionFromPostgresqlThrowable(C5001, ex)));
+ }
+
+ private Mono sendMessage(final TransactionEntity entity) {
+ return convertTransactionToJson(entity)
+ .flatMap(trxJson -> reactiveKafkaProducer.sendMessage(topic, UUID.randomUUID().toString(), trxJson)
+ .doOnError(ex -> log.error("Error sending message to Kafka", ex)))
+ .onErrorResume(ex -> Mono.empty());
+ }
+
+ private Mono convertTransactionToJson(final TransactionEntity entity) {
+ var rawTransaction = RawTransaction.builder()
+ .id(entity.getId())
+ .amount(entity.getAmount())
+ .build();
+
+ try {
+ return Mono.just(objectMapper.writeValueAsString(rawTransaction));
+ } catch (JsonProcessingException ex) {
+ log.error("Error processing transaction", ex);
+ return Mono.error(C5003.getException(ex));
+ }
+ }
+
+ private TransactionResponse buildTransactionResponse(final TransactionEntity entity) {
+ return new TransactionResponse()
+ .transactionExternalId(UUID.fromString(entity.getId()));
+ }
+
+ @Override
+ public Mono getTrx(final UUID transactionExternalId) {
+ return transactionRepository
+ .findById(transactionExternalId.toString())
+ .map(this::buildGetTransactionResponse)
+ .doOnError((Throwable ex) -> log.error("Error obtaining transaction", ex))
+ .onErrorResume((Throwable ex) ->
+ Mono.error(buildApiExceptionFromPostgresqlThrowable(C5001, ex)));
+ }
+
+ private GetTransactionResponse buildGetTransactionResponse(final TransactionEntity entity) {
+ return new GetTransactionResponse()
+ .transactionExternalId(UUID.fromString(entity.getId()))
+ .transactionType(new TransactionTypeResponse()
+ .code(entity.getTransactionTypeCode())
+ .name(applicationProperties.getTrxTypes().get(entity.getTransactionTypeCode())))
+ .amount(entity.getAmount())
+ .transactionStatus(new TransactionStatusResponse()
+ .name(entity.getStatus()))
+ .createdAt(entity.getCreatedAt())
+ .updatedAt(entity.getUpdatedAt());
+ }
+
+ @Override
+ public Mono updateTrx(final AnalysisResult analysisResult) {
+ return transactionRepository
+ .findById(analysisResult.getTransactionId())
+ .switchIfEmpty(Mono.defer(() -> Mono.error(C4091.getException())))
+ .flatMap(transactionEntity -> updateTransactionEntity(transactionEntity, analysisResult))
+ .onErrorMap(this::handleError)
+ .then();
+ }
+
+ private Mono updateTransactionEntity(final TransactionEntity transactionEntity,
+ final AnalysisResult analysisResult) {
+
+ String status = analysisResult.getIsFraudulent() ? applicationProperties
+ .getTrxStatuses().getRejected() : applicationProperties.getTrxStatuses().getApproved();
+
+ var trx = transactionEntity.toBuilder()
+ .status(status)
+ .updatedAt(LocalDateTime.now())
+ .build();
+ return transactionRepository.save(trx)
+ .doOnError(ex -> log.error("Error updating transaction in PostgresSQL: {}", ex.getMessage()));
+ }
+
+ private Throwable handleError(final Throwable ex) {
+ log.error("Error processing transaction update: {}", ex.getMessage());
+ return buildApiExceptionFromPostgresqlThrowable(C5001, ex);
+ }
+
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/ApplicationProperties.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/ApplicationProperties.java
new file mode 100644
index 0000000..7e24031
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/ApplicationProperties.java
@@ -0,0 +1,41 @@
+package com.bcp.services.transaction.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Map;
+
+/**
+ * Application Properties.
+ */
+@Getter
+@Setter
+@Configuration
+@ConfigurationProperties(prefix = "application")
+public class ApplicationProperties {
+
+ private Map trxTypes;
+ private TrxStatuses trxStatuses;
+
+ /**
+ * Class that contains Counterparty Type.
+ *
+ * @author Vito
+ * @version 1.0
+ */
+ @Getter
+ @Setter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class TrxStatuses {
+ private String pending;
+ private String approved;
+ private String rejected;
+ }
+}
\ No newline at end of file
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/CustomWebFluxConfigurer.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/CustomWebFluxConfigurer.java
new file mode 100644
index 0000000..165c32f
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/CustomWebFluxConfigurer.java
@@ -0,0 +1,34 @@
+package com.bcp.services.transaction.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.MediaType;
+import org.springframework.http.codec.json.Jackson2JsonDecoder;
+import org.springframework.http.codec.json.Jackson2JsonEncoder;
+import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
+import org.springframework.web.reactive.config.WebFluxConfigurer;
+import org.springframework.http.codec.ServerCodecConfigurer;
+
+/**
+ * Custom WebFlux Configurer.
+ */
+@Configuration
+public class CustomWebFluxConfigurer implements WebFluxConfigurer {
+
+ @Override
+ public void configureHttpMessageCodecs(final ServerCodecConfigurer configurer) {
+ configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper(), MediaType.APPLICATION_JSON));
+ configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper(), MediaType.APPLICATION_JSON));
+ }
+
+ @Bean
+ public ObjectMapper objectMapper() {
+ return Jackson2ObjectMapperBuilder.json()
+ .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // ISO-8601 format
+ .modules(new JavaTimeModule()) // Register the JavaTimeModule
+ .build();
+ }
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/ReactiveKafkaConsumerConfig.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/ReactiveKafkaConsumerConfig.java
new file mode 100644
index 0000000..af8c6f8
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/ReactiveKafkaConsumerConfig.java
@@ -0,0 +1,54 @@
+package com.bcp.services.transaction.config;
+
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import reactor.kafka.receiver.KafkaReceiver;
+import reactor.kafka.receiver.ReceiverOptions;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Reactive Kafka Consumer Configuration.
+ */
+@Configuration
+public class ReactiveKafkaConsumerConfig {
+
+ @Value("${spring.kafka.bootstrap-servers}")
+ private String bootstrapServers;
+
+ @Value("${spring.kafka.consumer.group-id}")
+ private String groupId;
+
+ @Value("${spring.kafka.consumer.auto-offset-reset}")
+ private String autoOffsetReset;
+
+ @Value("${spring.kafka.consumer.enable-auto-commit}")
+ private boolean enableAutoCommit;
+
+ @Value("${spring.kafka.consumer.key-deserializer}")
+ private String keyDeserializer;
+
+ @Value("${spring.kafka.consumer.value-deserializer}")
+ private String valueDeserializer;
+
+ @Value("${application.kafka.consumer.topic}")
+ private String topic;
+
+ @Bean
+ public KafkaReceiver kafkaReceiver() {
+ Map consumerProps = new HashMap<>();
+ consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, groupId);
+ consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset);
+ consumerProps.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, enableAutoCommit);
+ consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, keyDeserializer);
+ consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, valueDeserializer);
+
+ ReceiverOptions receiverOptions = ReceiverOptions.create(consumerProps);
+ return KafkaReceiver.create(receiverOptions.subscription(Collections.singleton(topic)));
+ }
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/ReactiveKafkaProducerConfig.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/ReactiveKafkaProducerConfig.java
new file mode 100644
index 0000000..f1abcdf
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/ReactiveKafkaProducerConfig.java
@@ -0,0 +1,38 @@
+package com.bcp.services.transaction.config;
+
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import reactor.kafka.sender.KafkaSender;
+import reactor.kafka.sender.SenderOptions;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Reactive Kafka Producer Configuration.
+ */
+@Configuration
+public class ReactiveKafkaProducerConfig {
+
+ @Value("${spring.kafka.bootstrap-servers}")
+ private String bootstrapServers;
+
+ @Value("${spring.kafka.producer.key-serializer}")
+ private String keySerializer;
+
+ @Value("${spring.kafka.producer.value-serializer}")
+ private String valueSerializer;
+
+ @Bean
+ public KafkaSender kafkaSender() {
+ Map producerProps = new HashMap<>();
+ producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
+ producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, keySerializer);
+ producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, valueSerializer);
+
+ SenderOptions senderOptions = SenderOptions.create(producerProps);
+ return KafkaSender.create(senderOptions);
+ }
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/config/ApplicationReadyListener.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/config/ApplicationReadyListener.java
new file mode 100644
index 0000000..9897a54
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/config/ApplicationReadyListener.java
@@ -0,0 +1,25 @@
+package com.bcp.services.transaction.config.core.utils.config;
+
+import com.bcp.services.transaction.config.core.utils.utils.PropertyUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.context.event.ApplicationReadyEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+/**
+ * ApplicationReadyListener.
+ * This class is used to listen to the application ready event.
+ */
+@Component
+@Slf4j
+public class ApplicationReadyListener implements ApplicationListener {
+
+ @Override
+ public void onApplicationEvent(final ApplicationReadyEvent event) {
+ log.trace("Configuring properties resolver...");
+ final Environment environment = event.getApplicationContext().getEnvironment();
+ PropertyUtils.setResolver(environment);
+ }
+
+}
\ No newline at end of file
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/constants/Constants.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/constants/Constants.java
new file mode 100644
index 0000000..e0b8eca
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/constants/Constants.java
@@ -0,0 +1,15 @@
+package com.bcp.services.transaction.config.core.utils.constants;
+
+/**
+ * Class for applications constants.
+ *
+ * @author vito.ivan
+ */
+public final class Constants {
+
+ private Constants() {
+ }
+
+ public static final String TECHNICAL_ERROR = "Technical";
+
+}
\ No newline at end of file
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/constants/ErrorCategory.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/constants/ErrorCategory.java
new file mode 100644
index 0000000..af97261
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/constants/ErrorCategory.java
@@ -0,0 +1,65 @@
+package com.bcp.services.transaction.config.core.utils.constants;
+
+import com.bcp.services.transaction.config.core.utils.utils.PropertyUtils;
+import lombok.Getter;
+
+import static com.bcp.services.transaction.config.core.utils.constants.Constants.TECHNICAL_ERROR;
+
+/**
+ * Dummy
+ * Class: {@link ErrorCategory}
+ *
+ * @author vito.ivan
+ * @version 1.0
+ */
+
+public enum ErrorCategory {
+
+ INVALID_REQUEST("invalid-request", 400),
+ ARGUMENT_MISMATCH("argument-mismatch", 400),
+ UNAUTHORIZED("unauthorized", 401),
+ FORBIDDEN("forbidden", 403),
+ RESOURCE_NOT_FOUND("resource-not-found", 404),
+ CONFLICT("conflict", 409),
+ PRECONDITION_FAILED("precondition-failed", 412),
+ EXTERNAL_ERROR("external-error", 500),
+ HOST_NOT_FOUND("host-not-found", 500),
+ UNEXPECTED("unexpected", 500),
+ NOT_IMPLEMENTED("not-implemented", 501),
+ SERVICE_UNAVAILABLE("service-unavailable", 503),
+ EXTERNAL_TIMEOUT("external-timeout", 503);
+
+ private static final String PROPERTY_PREFIX = "application.api.error-code.";
+ private final String property;
+ @Getter
+ private final int httpStatus;
+
+ ErrorCategory(final String property000, final int httpStatus000) {
+ this.property = PROPERTY_PREFIX.concat(property000);
+ this.httpStatus = httpStatus000;
+ }
+
+ private String codeProperty() {
+ return this.property + ".code";
+ }
+
+ private String descriptionProperty() {
+ return this.property + ".description";
+ }
+
+ private String errorTypeProperty() {
+ return this.property + ".error-type";
+ }
+
+ public String getErrorType() {
+ return PropertyUtils.getOptionalValue(this.errorTypeProperty()).orElse(TECHNICAL_ERROR);
+ }
+
+ public String getCode() {
+ return PropertyUtils.getOptionalValue(this.codeProperty()).orElse("TL9999");
+ }
+
+ public String getDescription() {
+ return PropertyUtils.getOptionalValue(this.descriptionProperty()).orElse("Sin descripcion configurada.");
+ }
+}
\ No newline at end of file
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/constants/HttpHeadersKey.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/constants/HttpHeadersKey.java
new file mode 100644
index 0000000..b28fcb4
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/constants/HttpHeadersKey.java
@@ -0,0 +1,20 @@
+package com.bcp.services.transaction.config.core.utils.constants;
+
+/**
+ * Dummy
+ * Class: {@link HttpHeadersKey}
+ *
+ * @author vito.ivan
+ * @version 1.0
+ */
+public final class HttpHeadersKey {
+
+ private HttpHeadersKey() {
+ }
+
+ public static final String REQUEST_ID = "request-id";
+ public static final String REQUEST_DATE = "request-date";
+ public static final String APP_CODE = "app-code";
+ public static final String CALLER_NAME = "caller-name";
+
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/exception/ApiException.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/exception/ApiException.java
new file mode 100644
index 0000000..a1b09c1
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/exception/ApiException.java
@@ -0,0 +1,267 @@
+package com.bcp.services.transaction.config.core.utils.exception;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.bcp.services.transaction.config.core.utils.constants.ErrorCategory;
+import com.bcp.services.transaction.config.core.utils.constants.HttpHeadersKey;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.MDC;
+import org.springframework.http.HttpHeaders;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Dummy
+ * Class: {@link ApiException}
+ *
+ * @author vito.ivan
+ * @version 1.0
+ */
+@SuppressFBWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED",
+ justification = "Transient field that isn't set by deserialization.")
+@Getter
+@JsonAutoDetect(creatorVisibility = Visibility.NONE,
+ fieldVisibility = Visibility.NONE, getterVisibility = Visibility.NONE,
+ isGetterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
+@JsonInclude(Include.NON_NULL)
+@Schema(description = "Datos del error de sistema.")
+@Slf4j
+public class ApiException extends RuntimeException {
+
+ @JsonProperty
+ @Schema(title = "Codigo de error de Sistema", example = "TL0001")
+ private final String code;
+
+ @JsonProperty
+ @Schema(title = "Descripcion del error de Sistema", example = "Error al llamar al servicio")
+ private final String description;
+
+ @JsonProperty
+ @Schema(title = "Tipo de Error de Sistema", example = "TECHNICAL")
+ private final String errorType;
+
+ @JsonProperty
+ @Schema(title = "Lista de detalles del error")
+ private final List exceptionDetails;
+
+ @JsonProperty
+ @JsonIgnore
+ private final Map properties;
+
+ @JsonIgnore
+ private final HttpHeaders headers;
+
+ @Schema(title = "Categoria del error", example = "INVALID_REQUEST")
+ @JsonProperty
+ private final ErrorCategory category;
+
+ @JsonIgnore
+ private final boolean isResolved;
+
+ @SuppressFBWarnings(
+ justification = "generated code"
+ )
+ @JsonCreator
+ private ApiException(
+ final @JsonProperty(value = "code", required = true) String code01,
+ final @JsonProperty(value = "description", required = true) String description01,
+ final @JsonProperty("errorType") String errorType01,
+ final @JsonProperty("exceptionDetails") List exceptionDetails01,
+ final @JsonProperty("properties") Map properties01) {
+
+ this.code = code01;
+ this.description = description01;
+ this.errorType = errorType01;
+ this.headers = null;
+ this.category = null;
+ this.exceptionDetails = Optional.ofNullable(exceptionDetails01).map(Collections::unmodifiableList).orElseGet(
+ Collections::emptyList);
+ this.properties = properties01;
+ this.isResolved = true;
+ }
+
+ @SuppressFBWarnings(
+ justification = "generated code"
+ )
+ ApiException(
+ final String code01,
+ final String description01,
+ final String errorType01,
+ final ErrorCategory category01,
+ final List exceptionDetails01,
+ final Map properties01,
+ final HttpHeaders headers01,
+ final Throwable cause01,
+ final boolean resolved01) {
+
+ super(description01, cause01);
+
+ this.code = code01;
+ this.description = description01;
+ this.errorType = errorType01;
+ this.category = category01;
+ this.exceptionDetails = Optional.ofNullable(exceptionDetails01).map(Collections::unmodifiableList)
+ .orElseGet(Collections::emptyList);
+ this.properties = properties01;
+ this.headers = headers01;
+ this.isResolved = resolved01;
+ if (headers != null) {
+ this.putMdc(headers);
+ }
+ MDC.put("code", code);
+ MDC.put("description", description);
+ MDC.put("errorType", errorType);
+ exceptionDetails.forEach(this::setMdcProperties);
+ if (properties != null) {
+ properties.forEach((key, value) -> MDC.put(key, value.toString()));
+ }
+ }
+
+ private void putMdc(final HttpHeaders headers0) {
+ List validHeaders = new ArrayList<>();
+ validHeaders.add(HttpHeadersKey.REQUEST_ID);
+ validHeaders.add(HttpHeadersKey.APP_CODE);
+ validHeaders.add(HttpHeadersKey.REQUEST_DATE);
+ validHeaders.add(HttpHeadersKey.CALLER_NAME);
+
+ if (headers0 != null) {
+ headers0.forEach((key, value) -> {
+ MDC.clear();
+ if (validHeaders.contains(key)) {
+ MDC.put(key, value.get(value.size() - 1));
+ } else {
+ log.debug("HttpHeadersKey not match");
+ }
+ });
+ }
+
+ }
+
+ private void setMdcProperties(final ApiExceptionDetail value) {
+ if (value.getCode() != null) {
+ MDC.put("exceptionDetails.code", value.getCode());
+ } else if (value.getComponent() != null) {
+ MDC.put("exceptionDetails.component", value.getComponent());
+ } else if (value.getDescription() != null) {
+ MDC.put("exceptionDetails.description", value.getDescription());
+ } else if (value.getEndpoint() != null) {
+ MDC.put("exceptionDetails.endpoint", value.getEndpoint());
+ }
+ }
+
+ /**
+ * If the parent {@code cause} is an {@link ApiException}, joins with the
+ * current list.
+ *
+ * @return A list of {@link ApiExceptionDetail}
+ */
+ @JsonProperty("exceptionDetails")
+ public List getExceptionDetails() {
+ if (getCause() instanceof ApiException apiException) {
+ List details = apiException.getExceptionDetails();
+ List newDetails = new ArrayList<>();
+ newDetails.addAll(exceptionDetails);
+ newDetails.addAll(details);
+ return Collections.unmodifiableList(newDetails);
+ }
+ return new ArrayList<>(exceptionDetails);
+ }
+
+ /**
+ * Get the original list of {@link ApiExceptionDetail} not including the
+ * {@link Throwable} cause.
+ *
+ * @return A list of {@link ApiExceptionDetail}
+ */
+ public List getUnresolvedExceptionDetails() {
+ return new ArrayList<>(exceptionDetails);
+ }
+
+ /**
+ * Create an ApiExceptionBuilder instance.
+ *
+ * @return An ApiExceptionBuilder instance.
+ */
+ public static ApiExceptionBuilder builder() {
+ return new ApiExceptionBuilder();
+ }
+
+ /**
+ * Create an ApiExceptionBuilder instance.
+ *
+ * @return An ApiExceptionBuilder instance.
+ */
+ public ApiExceptionBuilder mutate() {
+
+ ApiExceptionBuilder builder = builder()
+ .cause(isResolved() ? this : getCause())
+ .category(getCategory())
+ .errorType(getErrorType())
+ .systemCode(getCode())
+ .description(getDescription());
+
+ if (properties != null) {
+ properties.forEach(builder::addProperty);
+ }
+
+ if (headers != null) {
+ headers.forEach((key, value1) -> {
+ for (String value : value1) {
+ builder.addHeader(key, value);
+ }
+ });
+ }
+
+ builder.setMutated(true);
+
+ if (isResolved()) {
+ builder.markAsResolved();
+ } else {
+ if (null != exceptionDetails) {
+ exceptionDetails.forEach(detail -> builder.addDetail(detail.isResolved())
+ .withComponent(detail.getComponent())
+ .withEndpoint(detail.getEndpoint())
+ .withCode(detail.getCode())
+ .withDescription(detail.getDescription())
+ .push());
+ }
+ }
+ return builder;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("class ApiException {\n");
+ sb.append(" code: ").append(toIndentedString(code)).append("\n");
+ sb.append(" description: ").append(toIndentedString(description)).append("\n");
+ sb.append(" errorType: ").append(toIndentedString(errorType)).append("\n");
+ sb.append(" exceptionDetails: ").append(toIndentedString(exceptionDetails)).append("\n");
+ sb.append(" category: ").append(toIndentedString(category)).append("\n");
+ sb.append("}");
+ return sb.toString();
+ }
+
+ /**
+ * Convert the given object to string with each line indented by 4 spaces
+ * (except the first line).
+ */
+ private String toIndentedString(final Object o) {
+ if (o == null) {
+ return "null";
+ }
+ return o.toString().replace("\n", "\n ");
+ }
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/exception/ApiExceptionBuilder.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/exception/ApiExceptionBuilder.java
new file mode 100644
index 0000000..724135a
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/exception/ApiExceptionBuilder.java
@@ -0,0 +1,378 @@
+package com.bcp.services.transaction.config.core.utils.exception;
+
+import com.bcp.services.transaction.config.core.utils.constants.ErrorCategory;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import org.apache.commons.lang3.ClassUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpHeaders;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import static org.apache.commons.lang3.ClassUtils.isAssignable;
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
+
+/**
+ * Dummy
+ * Class: {@link ApiExceptionBuilder}
+ *
+ * @author vito.ivan
+ * @version 1.0
+ */
+@NoArgsConstructor(access = AccessLevel.PACKAGE)
+public final class ApiExceptionBuilder {
+
+ private String componentName;
+ private String endpoint;
+
+ private ErrorCategory category;
+ private String systemCode;
+ private String description;
+ private String errorType;
+ private Throwable cause;
+
+ private final List apiExceptionDetails = new ArrayList<>();
+ private final Map properties = new ConcurrentHashMap<>();
+ private final HttpHeaders headers = new HttpHeaders();
+
+ private boolean hasNewCategory = false;
+ private boolean hasNewErrorType = false;
+ private boolean hasNewSystemCode = false;
+ private boolean hasNewDescription = false;
+ private boolean hasNewProperty = false;
+ private boolean hasNewHeader = false;
+
+ private boolean isMutated = false;
+ private boolean isResolved = false;
+
+ public ApiExceptionBuilder setComponentName(final String componentName0) {
+ this.componentName = componentName0;
+ return this;
+ }
+
+ public ApiExceptionBuilder setEndpoint(final String endpoint0) {
+ this.endpoint = endpoint0;
+ return this;
+ }
+
+ /**
+ * Sets whether this builder is coming from an existing {@link ApiException}.
+ */
+ void setMutated(final boolean mutated0) {
+ isMutated = mutated0;
+ }
+
+ /**
+ * Checks whether this builder is coming from an existing
+ * {@link ApiException}.
+ *
+ * @return True if it is mutated.
+ */
+ public boolean isMutated() {
+ return isMutated;
+ }
+
+ /**
+ * Set error category for new ApiException.
+ *
+ * @param category0 Error category to configure.
+ * @return An ApiExceptionBuilder instance.
+ */
+ public ApiExceptionBuilder category(final ErrorCategory category0) {
+ if (category0 != null) {
+ this.hasNewCategory = true;
+ this.category = category0;
+ }
+ return this;
+ }
+
+ /**
+ * Set error type for new ApiException.
+ *
+ * @param errorType0 Error type to configure.
+ * @return An ApiExceptionBuilder instance.
+ */
+ public ApiExceptionBuilder errorType(final String errorType0) {
+ if (isNotBlank(errorType0)) {
+ this.hasNewErrorType = true;
+ this.errorType = errorType0;
+ }
+ return this;
+ }
+
+ /**
+ * Set description for new ApiException.
+ *
+ * @param description0 Description to configure.
+ * @return An ApiExceptionBuilder instance.
+ */
+ public ApiExceptionBuilder description(final String description0) {
+ if (isNotBlank(description0)) {
+ this.hasNewDescription = true;
+ this.description = description0;
+ }
+ return this;
+ }
+
+ /**
+ * Set System code for new ApiException.
+ *
+ * @param systemCode0 System code to configure.
+ * @return An ApiExceptionBuilder instance.
+ */
+ public ApiExceptionBuilder systemCode(final String systemCode0) {
+ if (isNotBlank(systemCode0)) {
+ this.hasNewSystemCode = true;
+ this.systemCode = systemCode0;
+ }
+ return this;
+ }
+
+ /**
+ * Insert a pair key-value as a HTTP Header in the exception.
+ *
+ * @param key0 The entry key associated.
+ * @param value0 The value associated to key.
+ */
+ public void addHeader(final String key0, final String value0) {
+ if (isNotBlank(key0) && StringUtils.isAsciiPrintable(key0)) {
+ this.hasNewHeader = true;
+ headers.add(key0, value0);
+ }
+ }
+
+ /**
+ * Insert a pair key-value as a custom field in the exception.
+ *
+ * @param key The entry key associated.
+ * @param value The value associated to key.
+ */
+ public void addProperty(final String key, final Object value) {
+ if (isNotBlank(key) && StringUtils.isAsciiPrintable(key) && isValidForProperty(value)) {
+ this.hasNewProperty = true;
+ properties.put(key, value);
+ }
+ }
+
+ private static boolean isValidForProperty(final Object value) {
+ return value != null && (ClassUtils.isPrimitiveOrWrapper(value.getClass())
+ || isAssignable(value.getClass(), CharSequence.class)
+ || isAssignable(value.getClass(), Date.class)
+ || isAssignable(value.getClass(), Calendar.class)
+ || isAssignable(value.getClass(), Instant.class)
+ || isAssignable(value.getClass(), Timestamp.class));
+ }
+
+ /**
+ * Set base exception for new ApiException.
+ *
+ * @param cause0 Base Exception to configure.
+ * @return An ApiExceptionBuilder instance.
+ */
+ public ApiExceptionBuilder cause(final Throwable cause0) {
+ this.cause = cause0;
+ return this;
+ }
+
+ private void addExceptionDetail(final ApiExceptionDetail detail) {
+ this.apiExceptionDetails.add(detail);
+ this.isResolved = false;
+ }
+
+ /**
+ * Mark new ApiException as resolved.
+ */
+ public void markAsResolved() {
+ this.isResolved = true;
+ }
+
+ /**
+ * Returns the builder for create a new {@link ApiExceptionDetail}.
+ *
+ * @return The {@link ApiExceptionDetailBuilder} instance.
+ */
+ public ApiExceptionDetailBuilder addDetail() {
+ return this.addDetail(false);
+ }
+
+ /**
+ * Returns the builder for create a new {@link ApiExceptionDetail}.
+ *
+ * @param resolved Whether the detail is marked as resolved or not.
+ * @return The {@link ApiExceptionDetailBuilder} instance.
+ */
+ public ApiExceptionDetailBuilder addDetail(final boolean resolved) {
+ return new ApiExceptionDetailBuilder(this, resolved);
+ }
+
+ /**
+ * dummy.
+ *
+ * @author willianmarchan
+ */
+ public static final class ApiExceptionDetailBuilder {
+
+ private String code;
+ private String component;
+ private String description;
+ private Boolean resolved;
+ private String endpoint;
+
+ private final ApiExceptionBuilder builder;
+
+ private ApiExceptionDetailBuilder(final ApiExceptionBuilder builder0, final boolean resolved0) {
+ this.builder = builder0;
+ this.resolved = resolved0;
+ }
+
+ /**
+ * Set the {@code code} for {@link ApiExceptionDetail}.
+ *
+ * @param code0 The component to be set.
+ * @return The {@link ApiExceptionDetailBuilder} instance.
+ */
+ public ApiExceptionDetailBuilder withCode(final String code0) {
+ if (isNotBlank(code0)) {
+ this.code = code0;
+ }
+ return this;
+ }
+
+ /**
+ * Set the {@code component} for {@link ApiExceptionDetail}.
+ *
+ * @param component0 The component to be set.
+ * @return The {@link ApiExceptionDetailBuilder} instance.
+ */
+ public ApiExceptionDetailBuilder withComponent(final String component0) {
+ if (isNotBlank(component0)) {
+ this.component = component0;
+ }
+ return this;
+ }
+
+ /**
+ * Set the {@code endpoint} for {@link ApiExceptionDetail}.
+ *
+ * @param endpoint0 The component to be set.
+ * @return The {@link ApiExceptionDetailBuilder} instance.
+ */
+ public ApiExceptionDetailBuilder withEndpoint(final String endpoint0) {
+ if (isNotBlank(endpoint0)) {
+ this.endpoint = endpoint0;
+ }
+ return this;
+ }
+
+ /**
+ * Set the {@code description} for {@link ApiExceptionDetail}.
+ *
+ * @param description0 The description to be set.
+ * @return The {@link ApiExceptionDetailBuilder} instance.
+ */
+ public ApiExceptionDetailBuilder withDescription(final String description0) {
+ if (isNotBlank(description0)) {
+ this.description = description0;
+ }
+ return this;
+ }
+
+ /**
+ * Creates and Adds a new {@link ApiExceptionDetail} with the parameters
+ * previosly setted.
+ *
+ * @return The {@link ApiExceptionBuilder} instance.
+ */
+ public ApiExceptionBuilder push() {
+ component = component == null ? this.builder.componentName : component;
+ endpoint = endpoint == null ? this.builder.endpoint : endpoint;
+ if (isNotBlank(code) || isNotBlank(component) || isNotBlank(description)
+ || isNotBlank(endpoint)) {
+ builder.addExceptionDetail(
+ new ApiExceptionDetail(code, component, description, resolved, endpoint));
+ this.code = null;
+ this.component = null;
+ this.description = null;
+ this.resolved = null;
+ }
+ return this.builder;
+ }
+ }
+
+ /**
+ * Create a new instance of ApiException.
+ *
+ * @return An ApiException instance.
+ */
+ public ApiException build() {
+
+ Boolean isResolvedFlag = isResolved;
+
+ if (cause != null) {
+ if (cause instanceof ApiException apiException) {
+ apiException.getUnresolvedExceptionDetails()
+ .forEach(detail -> addDetail(detail.isResolved())
+ .withCode(detail.getCode())
+ .withComponent(detail.getComponent())
+ .withDescription(detail.getDescription())
+ .withEndpoint(detail.getEndpoint())
+ .push());
+ cause(null);
+ } else if (!isMutated()) {
+ addDetail(true)
+ .withComponent(componentName)
+ .withEndpoint(endpoint)
+ .withDescription(cause.getClass().getName()
+ .concat(Optional.ofNullable(cause.getMessage()).map(" : "::concat).orElse("")))
+ .push();
+ cause(null);
+ }
+ }
+
+ return this.createApiExceptionInstance(isResolvedFlag);
+ }
+
+ private ApiException createApiExceptionInstance(final Boolean isResolvedFlag) {
+ return new ApiException(hasNewSystemCode ? systemCode : null,
+ // Avoid to call error catalog / with a previous code
+ hasNewDescription ? description : null,
+ hasNewErrorType ? errorType : null,
+ hasNewCategory ? category : ErrorCategory.UNEXPECTED,
+ apiExceptionDetails,
+ hasNewProperty ? properties : null,
+ hasNewHeader ? headers : null,
+ cause,
+ isResolvedFlag);
+ }
+
+ public Mono buildAsCompletable() {
+ return Mono.error(this::build);
+ }
+
+ public Mono buildAsSingle() {
+ return Mono.error(this::build);
+ }
+
+ public Flux buildAsObservable() {
+ return Flux.error(this::build);
+ }
+
+ public Flux buildAsMaybe() {
+ return Flux.error(this::build);
+ }
+
+ public Flux buildAsFlowable() {
+ return Flux.error(this::build);
+ }
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/exception/ApiExceptionDetail.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/exception/ApiExceptionDetail.java
new file mode 100644
index 0000000..30e934c
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/exception/ApiExceptionDetail.java
@@ -0,0 +1,67 @@
+package com.bcp.services.transaction.config.core.utils.exception;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+/**
+ * Dummy
+ * Class: {@link ApiExceptionDetail}
+ *
+ * @author vito.ivan
+ * @version 1.0
+ */
+@ToString
+@Getter
+@Setter
+@Builder
+@JsonInclude(Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+@Schema(description = "Datos del error técnico.")
+public class ApiExceptionDetail {
+
+ @Schema(title = "Codigo de error del Detalle/Proveedor", example = "TL0008")
+ private final String code;
+
+ @Schema(title = "Nombre del componente de falla", example = "biz-sinister-query-compensationclaim-lpg-v1")
+ private final String component;
+
+ @Schema(title = "Descripcion del Detalle", example = "Codigo invalido para el canal")
+ private final String description;
+
+ @JsonIgnore
+ private final boolean resolved;
+
+ @Schema(title = "Endpoint que ejecuta el servicio", example = "(GET) /biz/sinister/query/compensation-claim/lpg/v1")
+ private final String endpoint;
+
+ /**
+ * Create an ApiExceptionDetail instance.
+ *
+ * @param code0 Provider error code.
+ * @param component0 Service provider code.
+ * @param description0 Provider error description.
+ * @param resolved0 Whether the detail will be resolved or not.
+ */
+ @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
+ ApiExceptionDetail(
+ @JsonProperty("code") final String code0,
+ @JsonProperty("component") final String component0,
+ @JsonProperty("description") final String description0,
+ @JsonProperty("resolved") final boolean resolved0,
+ @JsonProperty("endpoint") final String endpoint0) {
+ this.code = code0;
+ this.component = component0;
+ this.description = description0;
+ this.resolved = resolved0;
+ this.endpoint = endpoint0;
+ }
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/utils/PropertyUtils.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/utils/PropertyUtils.java
new file mode 100644
index 0000000..d86df5a
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/utils/utils/PropertyUtils.java
@@ -0,0 +1,47 @@
+package com.bcp.services.transaction.config.core.utils.utils;
+
+import org.springframework.core.env.PropertyResolver;
+
+import java.util.Optional;
+
+/**
+ * Dummy
+ * Class: {@link PropertyUtils}
+ *
+ * @author vito.ivan
+ * @version 1.0
+ */
+public final class PropertyUtils {
+ private PropertyUtils() {
+ }
+
+ private static final String APPLICATION_CODE_PROPERTY = "spring.application.name";
+ private static final String DEFAULT_APPLICATION_CODE = "unknown";
+
+ private static PropertyResolver resolver;
+
+ public static void setResolver(final PropertyResolver resolver0) {
+ PropertyUtils.resolver = resolver0;
+ }
+
+ public static String getValue(final String property) {
+
+ return resolver.getProperty(property);
+ }
+
+ public static T getValue(final String property, final Class resolvedClazz) {
+ return resolver.getProperty(property, resolvedClazz);
+ }
+
+ public static Optional getOptionalValue(final String property, final Class resolvedClazz) {
+ return Optional.ofNullable(resolver.getProperty(property, resolvedClazz));
+ }
+
+ public static Optional getOptionalValue(final String property) {
+ return Optional.ofNullable(resolver.getProperty(property));
+ }
+
+ public static String getApplicationCode() {
+ return getOptionalValue(APPLICATION_CODE_PROPERTY).orElse(DEFAULT_APPLICATION_CODE);
+ }
+}
\ No newline at end of file
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/web/ErrorCategoryFromThrowable.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/web/ErrorCategoryFromThrowable.java
new file mode 100644
index 0000000..f8e5a61
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/web/ErrorCategoryFromThrowable.java
@@ -0,0 +1,41 @@
+package com.bcp.services.transaction.config.core.web;
+
+import com.bcp.services.transaction.config.core.utils.constants.ErrorCategory;
+
+import java.net.ConnectException;
+import java.net.MalformedURLException;
+import java.net.NoRouteToHostException;
+import java.net.SocketException;
+import java.net.SocketTimeoutException;
+import java.net.URISyntaxException;
+import java.net.UnknownHostException;
+import java.util.concurrent.TimeoutException;
+
+import static org.springframework.util.ClassUtils.isAssignable;
+
+/**
+ * ErrorCategoryFromThrowable.
+ * This class is used to map exceptions to error categories.
+ */
+public final class ErrorCategoryFromThrowable {
+
+ private ErrorCategoryFromThrowable() {
+ }
+
+ public static ErrorCategory mapExceptionToCategory(final Exception ex) {
+ Class extends Exception> exClass = ex.getClass();
+ if (isAssignable(exClass, UnknownHostException.class)
+ || isAssignable(exClass, NoRouteToHostException.class)
+ || isAssignable(exClass, MalformedURLException.class)
+ || isAssignable(exClass, URISyntaxException.class)) {
+ return ErrorCategory.HOST_NOT_FOUND;
+ } else if (isAssignable(exClass, SocketTimeoutException.class)
+ || isAssignable(exClass, SocketException.class)
+ || isAssignable(exClass, TimeoutException.class)) {
+ return ErrorCategory.EXTERNAL_TIMEOUT;
+ } else if (isAssignable(exClass, ConnectException.class)) {
+ return ErrorCategory.SERVICE_UNAVAILABLE;
+ }
+ return ErrorCategory.UNEXPECTED;
+ }
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/web/ErrorControllerAdvice.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/web/ErrorControllerAdvice.java
new file mode 100644
index 0000000..37d5634
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/config/core/web/ErrorControllerAdvice.java
@@ -0,0 +1,145 @@
+package com.bcp.services.transaction.config.core.web;
+
+import com.bcp.services.transaction.config.core.utils.constants.ErrorCategory;
+import com.bcp.services.transaction.config.core.utils.exception.ApiException;
+import com.bcp.services.transaction.config.core.utils.utils.PropertyUtils;
+import jakarta.validation.ConstraintViolationException;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.bind.support.WebExchangeBindException;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import static com.bcp.services.transaction.config.core.utils.constants.ErrorCategory.INVALID_REQUEST;
+
+/**
+ * ErrorControllerAdvice.
+ * This class is used to handle exceptions.
+ */
+@RestControllerAdvice
+public class ErrorControllerAdvice {
+
+ @ExceptionHandler(ApiException.class)
+ protected Mono> handleCustomFrameworkException(
+ final ApiException exception,
+ final ServerWebExchange serverWebExchange) {
+ if (StringUtils.isBlank(exception.getCode())
+ && ((!exception.getExceptionDetails().isEmpty()
+ && StringUtils.isBlank(exception.getExceptionDetails().get(0).getCode()))
+ || exception.getExceptionDetails().isEmpty())) {
+ var endpoint = serverWebExchange.getRequest().getMethod().name()
+ .concat(StringUtils.SPACE).concat(serverWebExchange.getRequest().getPath().value());
+
+ var apiException = ApiException
+ .builder()
+ .category(exception.getCategory())
+ .systemCode(exception.getCategory().getCode())
+ .description(exception.getCategory().getDescription())
+ .errorType(exception.getCategory().getErrorType())
+ .cause(exception)
+ .addDetail(true)
+ .withEndpoint(endpoint)
+ .withComponent(PropertyUtils.getApplicationCode())
+ .push()
+ .build();
+ return Mono.just(ResponseEntity.status(this.fromCategoryToHttpStatus(apiException.getCategory()))
+ .body(apiException));
+ }
+ return Mono.just(ResponseEntity
+ .status(this.fromCategoryToHttpStatus(exception.getCategory()))
+ .body(exception.mutate().build()));
+ }
+
+ private HttpStatus fromCategoryToHttpStatus(final ErrorCategory errorCategory) {
+ if (errorCategory == null) {
+ return HttpStatus.INTERNAL_SERVER_ERROR;
+ } else {
+ return HttpStatus.valueOf(errorCategory.getHttpStatus());
+ }
+ }
+
+ @ExceptionHandler(ConstraintViolationException.class)
+ protected Mono> constraintViolationExceptionHandler(
+ final ConstraintViolationException violationException,
+ final ServerWebExchange serverWebExchange) {
+
+ var endpoint = serverWebExchange.getRequest().getMethod().name()
+ .concat(StringUtils.SPACE).concat(serverWebExchange.getRequest().getPath().value());
+
+ var finalBuilder = ApiException.builder()
+ .category(INVALID_REQUEST)
+ .systemCode(INVALID_REQUEST.getCode())
+ .description(INVALID_REQUEST.getDescription())
+ .errorType(INVALID_REQUEST.getErrorType())
+ .setComponentName(PropertyUtils.getApplicationCode())
+ .setEndpoint(endpoint);
+ violationException.getConstraintViolations()
+ .forEach(constraintViolation -> finalBuilder.addDetail()
+ .withDescription(constraintViolation.getMessage()).push());
+
+ var apiException = finalBuilder.build();
+ return Mono.just(ResponseEntity
+ .status(this.getHttpStatusFromCategory(apiException.getCategory()))
+ .body(apiException));
+ }
+
+ @ExceptionHandler(WebExchangeBindException.class)
+ protected Mono> webExchangeBindExceptionHandler(
+ final WebExchangeBindException webExchangeBindException,
+ final ServerWebExchange serverWebExchange) {
+
+ var endpoint = serverWebExchange.getRequest().getMethod().name()
+ .concat(StringUtils.SPACE).concat(serverWebExchange.getRequest().getPath().value());
+
+ var finalBuilder = ApiException.builder()
+ .setComponentName(PropertyUtils.getApplicationCode())
+ .errorType(INVALID_REQUEST.getErrorType())
+ .setEndpoint(endpoint)
+ .systemCode(INVALID_REQUEST.getCode())
+ .description(INVALID_REQUEST.getDescription())
+ .category(INVALID_REQUEST);
+ webExchangeBindException.getAllErrors()
+ .forEach(objectError -> finalBuilder.addDetail()
+ .withDescription(objectError.getDefaultMessage()).push());
+
+ var apiException = finalBuilder.build();
+ return Mono.just(ResponseEntity
+ .status(this.getHttpStatusFromCategory(apiException.getCategory()))
+ .body(apiException));
+ }
+
+ private HttpStatus getHttpStatusFromCategory(final ErrorCategory errorCategory) {
+ if (errorCategory == null) {
+ return HttpStatus.INTERNAL_SERVER_ERROR;
+ } else {
+ return HttpStatus.valueOf(errorCategory.getHttpStatus());
+ }
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity handleCustomException(final Exception exception,
+ final ServerWebExchange serverWebExchange) {
+
+ var endpoint = serverWebExchange.getRequest().getMethod().name()
+ .concat(StringUtils.SPACE).concat(serverWebExchange.getRequest().getPath().value());
+
+ var errorCategory = ErrorCategoryFromThrowable.mapExceptionToCategory(exception);
+
+ var apiException = ApiException.builder()
+ .setComponentName(PropertyUtils.getApplicationCode())
+ .errorType(errorCategory.getErrorType())
+ .setEndpoint(endpoint)
+ .systemCode(errorCategory.getCode())
+ .description(errorCategory.getDescription())
+ .category(errorCategory)
+ .cause(exception)
+ .build();
+
+ return ResponseEntity.status(this.fromCategoryToHttpStatus(apiException.getCategory()))
+ .body(apiException);
+ }
+
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/expose/web/TransactionApiImpl.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/expose/web/TransactionApiImpl.java
new file mode 100644
index 0000000..8750c25
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/expose/web/TransactionApiImpl.java
@@ -0,0 +1,53 @@
+package com.bcp.services.transaction.expose.web;
+
+import com.bcp.services.transaction.business.TransactionService;
+import com.bcp.services.transaction.trx.api.TransactionApiDelegate;
+import com.bcp.services.transaction.trx.model.GetTransactionResponse;
+import com.bcp.services.transaction.trx.model.TransactionRequest;
+import com.bcp.services.transaction.trx.model.TransactionResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Component;
+import org.springframework.web.server.ServerWebExchange;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+/**
+ * TransactionApiImpl.
+ * This class implements the TransactionApiDelegate interface.
+ */
+@RequiredArgsConstructor
+@Component
+public class TransactionApiImpl implements TransactionApiDelegate {
+
+ private final TransactionService transactionService;
+
+ @Override
+ public Mono> createTransaction(final UUID requestID,
+ final java.time.LocalDateTime requestDate,
+ final String callerName,
+ final Mono transactionRequest,
+ final ServerWebExchange exchange) {
+
+ return transactionService.createTrx(transactionRequest)
+ .map(response -> new ResponseEntity<>(response, org.springframework.http.HttpStatus.CREATED));
+
+ }
+
+ @Override
+ public Mono> getTransaction(final UUID requestID,
+ final java.time.LocalDateTime requestDate,
+ final String callerName,
+ final UUID transactionExternalId,
+ final ServerWebExchange exchange) {
+
+ return transactionService.getTrx(transactionExternalId)
+ .map(ResponseEntity::ok)
+ .defaultIfEmpty(ResponseEntity.noContent().build());
+ }
+
+}
+
+
+
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/AnalysisResult.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/AnalysisResult.java
new file mode 100644
index 0000000..dc86c76
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/AnalysisResult.java
@@ -0,0 +1,20 @@
+package com.bcp.services.transaction.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+/**
+ * AnalysisResult.
+ * This class is used to represent an AnalysisResult.
+ */
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class AnalysisResult {
+ private String transactionId;
+ private Boolean isFraudulent;
+ private String reason;
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/RawTransaction.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/RawTransaction.java
new file mode 100644
index 0000000..baa5985
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/RawTransaction.java
@@ -0,0 +1,22 @@
+package com.bcp.services.transaction.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.math.BigDecimal;
+
+/**
+ * RawTransaction.
+ * This class is used to represent a RawTransaction.
+ */
+@Builder
+@Getter
+@NoArgsConstructor
+@AllArgsConstructor
+public class RawTransaction {
+ private String id;
+ private BigDecimal amount;
+ private String currency;
+}
\ No newline at end of file
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/TransactionDto.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/TransactionDto.java
new file mode 100644
index 0000000..edddbe3
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/TransactionDto.java
@@ -0,0 +1,26 @@
+package com.bcp.services.transaction.model;
+
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.AllArgsConstructor;
+
+import java.math.BigDecimal;
+import java.util.UUID;
+
+@Builder
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class TransactionDto {
+ private UUID id;
+ private UUID accountExternalIdDebit;
+
+ private UUID accountExternalIdCredit;
+
+ private String transferTypeId;
+
+ private BigDecimal value;
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/TransactionEntity.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/TransactionEntity.java
new file mode 100644
index 0000000..0b17e90
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/model/TransactionEntity.java
@@ -0,0 +1,44 @@
+package com.bcp.services.transaction.model;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * Transaction Entity.
+ * This class is used to represent a Transaction.
+ */
+@Table("trx.transaction")
+@Builder(toBuilder = true)
+@Getter
+@Setter
+@NoArgsConstructor
+@AllArgsConstructor
+public class TransactionEntity {
+
+ @Id
+ private String id;
+ @Column("accountExternalIdDebit")
+ private String accountExternalIdDebit;
+ @Column("accountExternalIdCredit")
+ private String accountExternalIdCredit;
+ @Column("transactionTypeCode")
+ private String transactionTypeCode;
+ @Column("amount")
+ private BigDecimal amount;
+ @Column("status")
+ private String status;
+ @Column("createdAt")
+ private LocalDateTime createdAt;
+ @Column("updatedAt")
+ private LocalDateTime updatedAt;
+
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/repository/TransactionRepository.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/repository/TransactionRepository.java
new file mode 100644
index 0000000..3eab004
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/repository/TransactionRepository.java
@@ -0,0 +1,17 @@
+package com.bcp.services.transaction.repository;
+
+import com.bcp.services.transaction.model.TransactionEntity;
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+
+/**
+ * Transaction Postgresql Repository.
+ * This interface is used to interact with the Postgresql database.
+ * It extends the ReactiveCrudRepository interface.
+ *
+ * @see ReactiveCrudRepository
+ * @see TransactionEntity
+ * @see TransactionRepository
+ */
+public interface TransactionRepository extends ReactiveCrudRepository {
+
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/util/constants/Constants.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/util/constants/Constants.java
new file mode 100644
index 0000000..eef9709
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/util/constants/Constants.java
@@ -0,0 +1,20 @@
+package com.bcp.services.transaction.util.constants;
+
+/**
+ * Class containing constants.
+ *
+ * @author vito
+ */
+
+public final class Constants {
+ private Constants() {
+ }
+
+ /**
+ * The component name for the PostgresSQL database.
+ */
+ public static final String BD_COMPONENT_NAME = "PostgresSQL";
+
+ public static final String DOT_AND_SPACE = ". ";
+
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/util/exception/CustomApiException.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/util/exception/CustomApiException.java
new file mode 100644
index 0000000..99dfad2
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/util/exception/CustomApiException.java
@@ -0,0 +1,117 @@
+package com.bcp.services.transaction.util.exception;
+
+import com.bcp.services.transaction.config.core.utils.constants.ErrorCategory;
+import com.bcp.services.transaction.config.core.utils.exception.ApiException;
+import com.bcp.services.transaction.config.core.utils.utils.PropertyUtils;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Optional;
+
+import static com.bcp.services.transaction.config.core.utils.constants.ErrorCategory.CONFLICT;
+import static com.bcp.services.transaction.config.core.utils.constants.ErrorCategory.EXTERNAL_ERROR;
+import static com.bcp.services.transaction.config.core.utils.constants.ErrorCategory.UNEXPECTED;
+import static com.bcp.services.transaction.util.constants.Constants.BD_COMPONENT_NAME;
+import static com.bcp.services.transaction.util.constants.Constants.DOT_AND_SPACE;
+import static org.apache.logging.log4j.util.Strings.EMPTY;
+
+
+/**
+ * Builder component that contains methods to build exception object.
+ *
+ * @author vito.ivan
+ */
+@SuppressFBWarnings(value = "NM_CLASS_NOT_EXCEPTION", justification = "This class is derived from another exception")
+@Getter
+@AllArgsConstructor
+public enum CustomApiException {
+
+ C5001(EXTERNAL_ERROR,
+ "C5001", BD_COMPONENT_NAME,
+ "Error de base de datos. "),
+
+ C4091(CONFLICT,
+ "C4091", PropertyUtils.getApplicationCode(),
+ "No se encontró ningúna transacción para el ID proporcionado."),
+
+ C5003(UNEXPECTED,
+ "C5003", PropertyUtils.getApplicationCode(),
+ "Error al convertir un objeto a json. ");;
+
+ private final ErrorCategory category;
+
+ private final String code;
+ private final String componentName;
+ private final String description;
+
+ /**
+ * Return exception.
+ *
+ * @return ApiException
+ */
+ public ApiException getException() {
+
+ return ApiException
+ .builder()
+ .category(this.getCategory())
+ .systemCode(this.getCategory().getCode())
+ .description(this.getCategory().getDescription())
+ .errorType(this.getCategory().getErrorType())
+ .addDetail(true)
+ .withCode(this.getCode())
+ .withComponent(this.getComponentName())
+ .withDescription(this.getDescription())
+ .push()
+ .build();
+ }
+
+ /**
+ * Build and return a custom exception with description.
+ *
+ * @param throwable throwable
+ * @return ApiException
+ */
+ public ApiException getException(final Throwable throwable) {
+
+ return ApiException
+ .builder()
+ .category(this.getCategory())
+ .systemCode(this.getCategory().getCode())
+ .description(this.getCategory().getDescription())
+ .errorType(this.getCategory().getErrorType())
+ .addDetail(true)
+ .withCode(this.getCode())
+ .withComponent(this.getComponentName())
+ .withDescription(this.getDescription().concat(throwable.getClass().getName())
+ .concat(Optional.ofNullable(throwable.getMessage())
+ .map(DOT_AND_SPACE::concat).orElse(EMPTY))
+ .concat(Optional.ofNullable(throwable.getCause())
+ .map(th -> DOT_AND_SPACE.concat(th.getMessage())).orElse(EMPTY)))
+ .push()
+ .build();
+ }
+
+ /**
+ * Build and return a custom exception with description.
+ *
+ * @param throwable throwable
+ * @return ApiException
+ */
+ public ApiException getException(final String throwable) {
+
+ return ApiException
+ .builder()
+ .category(this.getCategory())
+ .systemCode(this.getCategory().getCode())
+ .description(this.getCategory().getDescription())
+ .errorType(this.getCategory().getErrorType())
+ .addDetail(true)
+ .withCode(this.getCode())
+ .withComponent(this.getComponentName())
+ .withDescription(this.getDescription().concat(throwable))
+ .push()
+ .build();
+ }
+
+}
diff --git a/business-transaction-v1/src/main/java/com/bcp/services/transaction/util/exception/ExceptionUtils.java b/business-transaction-v1/src/main/java/com/bcp/services/transaction/util/exception/ExceptionUtils.java
new file mode 100644
index 0000000..007588f
--- /dev/null
+++ b/business-transaction-v1/src/main/java/com/bcp/services/transaction/util/exception/ExceptionUtils.java
@@ -0,0 +1,99 @@
+package com.bcp.services.transaction.util.exception;
+
+import com.bcp.services.transaction.config.core.utils.exception.ApiException;
+import org.springframework.dao.DataAccessResourceFailureException;
+import org.springframework.jdbc.BadSqlGrammarException;
+
+import java.util.Map;
+import java.util.Optional;
+
+import static com.bcp.services.transaction.util.constants.Constants.DOT_AND_SPACE;
+import static org.apache.logging.log4j.util.Strings.EMPTY;
+
+/**
+ * Utility class for exceptions.
+ */
+public final class ExceptionUtils {
+ private ExceptionUtils() {
+ }
+
+
+ /**
+ * Build an API exception from a PostgreSQL throwable.
+ *
+ * @param customApiException the custom API exception.
+ * @param ex the throwable.
+ * @return the API exception.
+ */
+ public static ApiException buildApiExceptionFromPostgresqlThrowable(
+ final CustomApiException customApiException,
+ final Throwable ex) {
+
+ DbExceptionHandler handler = HANDLERS.get(ex.getClass());
+
+ if (handler != null) {
+ return customApiException.getException(handler.description(ex));
+ }
+
+ Throwable cause = ex.getCause();
+
+ if (cause != null) {
+ handler = HANDLERS.get(cause.getClass());
+
+ if (handler != null) {
+ return customApiException.getException(handler.description(cause));
+ }
+ }
+
+ return customApiException.getException(ex);
+ }
+
+ private static final Map, DbExceptionHandler> HANDLERS =
+ Map.of(DataAccessResourceFailureException.class, new DataAccessResourceFailureExceptionHandler(),
+ BadSqlGrammarException.class, new BadSqlGrammarExceptionHandler());
+
+ /**
+ * Interface that contains methods to build custom exception object.
+ *
+ * @author VI
+ */
+ public interface DbExceptionHandler {
+
+ String description(Throwable throwable);
+
+ }
+
+ private static class BadSqlGrammarExceptionHandler implements DbExceptionHandler {
+
+ @Override
+ public String description(final Throwable ex) {
+ if (ex instanceof BadSqlGrammarException exception) {
+
+ return exception.getClass().getName()
+ .concat(Optional.ofNullable(exception.getMessage())
+ .map(DOT_AND_SPACE::concat).orElse(EMPTY))
+ .concat(Optional.ofNullable(exception.getCause())
+ .map(th -> DOT_AND_SPACE.concat(th.getMessage())).orElse(EMPTY));
+ }
+ return EMPTY;
+ }
+
+ }
+
+ private static class DataAccessResourceFailureExceptionHandler implements DbExceptionHandler {
+
+ @Override
+ public String description(final Throwable ex) {
+ if (ex instanceof DataAccessResourceFailureException exception) {
+ return exception.getClass().getName()
+ .concat(Optional.ofNullable(exception.getMessage())
+ .map(DOT_AND_SPACE::concat).orElse(EMPTY))
+ .concat(Optional.ofNullable(exception.getCause())
+ .map(th -> DOT_AND_SPACE.concat(th.getMessage())).orElse(EMPTY));
+ }
+ return EMPTY;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/business-transaction-v1/src/main/resources/ValidationMessages.properties b/business-transaction-v1/src/main/resources/ValidationMessages.properties
new file mode 100644
index 0000000..66386e7
--- /dev/null
+++ b/business-transaction-v1/src/main/resources/ValidationMessages.properties
@@ -0,0 +1,2 @@
+transferTypeId=El campo transferTypeId debe coincidir con los valores permitidos: 220, 225, 320 o 325.
+transactionRequestMono.transferTypeId=El campo transferTypeId debe coincidir con los valores permitidos: 220, 225, 320 o 325.
\ No newline at end of file
diff --git a/business-transaction-v1/src/main/resources/application-local.yml b/business-transaction-v1/src/main/resources/application-local.yml
new file mode 100644
index 0000000..40c6151
--- /dev/null
+++ b/business-transaction-v1/src/main/resources/application-local.yml
@@ -0,0 +1,20 @@
+application:
+ kafka:
+ producer:
+ topic: transaction-anti-fraud-validation
+ consumer:
+ group-id: reactive-group
+ topic: transaction-processing
+ trx-types:
+ 220: Transferencias ordinaria
+ 225: Pago a cta tarjeta
+ 320: ORDINARIAS - INMEDIATAS
+ 325: PAGO DE TARJETA DE CREDITO - INMEDIATAS
+ trx-statuses:
+ pending: PENDING
+ approved: APPROVED
+ rejected: REJECTED
+logging:
+ level:
+ root: ERROR
+ com.bcp.services.transaction: TRACE
\ No newline at end of file
diff --git a/business-transaction-v1/src/main/resources/application.yml b/business-transaction-v1/src/main/resources/application.yml
new file mode 100644
index 0000000..7b398b6
--- /dev/null
+++ b/business-transaction-v1/src/main/resources/application.yml
@@ -0,0 +1,50 @@
+springdoc:
+ swagger-ui:
+ path: /swagger-ui.html
+ api-docs:
+ path: /openapi
+ enabled: true
+ show-actuator: true
+management:
+ endpoints:
+ web:
+ exposure:
+ include: health,info,openapi,swagger-ui,mappings
+spring:
+ application:
+ name: business-transaction
+ webflux:
+ base-path: /transaction/v1
+ 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: reactive-group
+ auto-offset-reset: earliest
+ enable-auto-commit: false
+ key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+ r2dbc:
+ url: r2dbc:postgresql://localhost:5432/mypostgresdb
+ username: postgresuser
+ password: postgrespassword
+ sql:
+ init:
+ mode: always
+ schema-locations: classpath:/data.sql
+ continue-on-error: true
+ jpa:
+ defer-datasource-initialization: true
+ main:
+ allow-bean-definition-overriding: true
+ messages:
+ basename: ValidationMessages
+ profiles:
+ active: local
+openapi:
+ aPIBusinessTransactionV1:
+ base-path: ''
+server:
+ port: 8085
\ No newline at end of file
diff --git a/business-transaction-v1/src/main/resources/data.sql b/business-transaction-v1/src/main/resources/data.sql
new file mode 100644
index 0000000..e39e676
--- /dev/null
+++ b/business-transaction-v1/src/main/resources/data.sql
@@ -0,0 +1,18 @@
+CREATE SCHEMA IF NOT EXISTS trx;
+
+
+DROP TABLE IF EXISTS trx.transaction;
+
+
+CREATE TABLE trx.transaction(id varchar(36) PRIMARY KEY,
+ accountExternalIdDebit varchar(40),
+ accountExternalIdCredit varchar(40),
+ transactionTypeCode varchar(20),
+ amount numeric,
+ status varchar(10),
+ createdAt timestamp,
+ updatedAt timestamp);
+
+GRANT ALL PRIVILEGES ON SCHEMA trx TO postgresuser;
+
+GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA trx TO postgresuser;
\ No newline at end of file
diff --git a/business-transaction-v1/src/main/resources/openapi.yaml b/business-transaction-v1/src/main/resources/openapi.yaml
new file mode 100644
index 0000000..ef4236c
--- /dev/null
+++ b/business-transaction-v1/src/main/resources/openapi.yaml
@@ -0,0 +1,277 @@
+openapi: 3.0.3
+info:
+ title: API Business Transaction V1
+ description: API que permite crear y recuperar transacciones financieras.
+ version: 1.0.0
+
+servers:
+ - url: http://localhost:8085
+ description: Servidor local
+
+tags:
+ - name: Transaction
+ description: Operaciones relacionadas con transacciones financieras
+
+components:
+ securitySchemes:
+ bearerAuth:
+ type: http
+ scheme: bearer
+ bearerFormat: JWT
+
+ parameters:
+ Request-ID:
+ name: Request-ID
+ in: header
+ description: ID único para identificar la solicitud.
+ required: true
+ schema:
+ type: string
+ format: uuid
+ example: 550e8400-e29b-41d4-a716-446655440000
+
+ request-date:
+ name: request-date
+ in: header
+ description: Fecha y hora de la solicitud.
+ required: true
+ schema:
+ type: string
+ format: date-time
+ example: '2020-10-09T14:02:03.987-0700'
+
+ caller-name:
+ name: caller-name
+ in: header
+ description: Nombre de la API que realiza la invocación al servicio.
+ required: true
+ schema:
+ type: string
+ pattern: '^[0-9a-zA-Z.-]*$'
+ maxLength: 100
+ minLength: 5
+ example: business-product-directory-v1
+
+ schemas:
+ TransactionRequest:
+ type: object
+ properties:
+ accountExternalIdDebit:
+ type: string
+ format: uuid
+ description: ID externo de la cuenta desde la cual se debitarán los fondos.
+ accountExternalIdCredit:
+ type: string
+ format: uuid
+ description: ID externo de la cuenta a la cual se acreditarán los fondos.
+ transactionTypeCode:
+ type: string
+ pattern: ^(220|225|320|325)$
+ description: Codigo del tipo de transferencia (220, 225, 320, 325).
+ amount:
+ type: string
+ format: decimal
+ description: Valor monetario de la transacción.
+ required:
+ - accountExternalIdDebit
+ - accountExternalIdCredit
+ - transactionTypeCode
+ - amount
+
+ TransactionResponse:
+ type: object
+ properties:
+ transactionExternalId:
+ type: string
+ format: uuid
+ description: ID externo asignado a la transacción.
+
+ GetTransactionResponse:
+ type: object
+ properties:
+ transactionExternalId:
+ type: string
+ format: uuid
+ description: ID externo asignado a la transacción.
+ transactionType:
+ $ref: '#/components/schemas/TransactionTypeResponse'
+ transactionStatus:
+ $ref: '#/components/schemas/TransactionStatusResponse'
+ amount:
+ type: string
+ format: decimal
+ description: Valor monetario de la transacción.
+ createdAt:
+ type: string
+ format: date-time
+ description: Fecha y hora en que se creó la transacción.
+ updatedAt:
+ type: string
+ format: date-time
+ description: Fecha y hora en que se actualizó la transacción.
+
+ TransactionTypeResponse:
+ type: object
+ properties:
+ code:
+ type: string
+ example: 220
+ description: Codigo del tipo de transferencia.
+ name:
+ type: string
+ example: Transferencia ordinaria
+ description: Nombre del tipo de transacción.
+ TransactionStatusResponse:
+ type: object
+ properties:
+ name:
+ type: string
+ example: pending
+ description: Estado de la transacción.
+
+
+ ApiException:
+ type: object
+ required:
+ - code
+ - description
+ - errorType
+ properties:
+ code:
+ type: string
+ description: Código de error del sistema.
+ example: TL0001
+ description:
+ type: string
+ description: Descripción del error del sistema.
+ example: Error al llamar al servicio
+ errorType:
+ type: string
+ description: Tipo de error (ej. TECHNICAL, BUSINESS).
+ example: TECHNICAL
+ exceptionDetails:
+ type: array
+ description: Lista de detalles del error.
+ items:
+ $ref: '#/components/schemas/ApiExceptionDetail'
+ properties:
+ type: object
+ description: Lista de propiedades adicionales específicas del error.
+ additionalProperties: true
+
+ ApiExceptionDetail:
+ type: object
+ properties:
+ code:
+ type: string
+ description: Código de error específico del detalle/proveedor.
+ example: MB0008
+ component:
+ type: string
+ description: Nombre del componente donde ocurrió el error.
+ example: MB.CardInqV2
+ description:
+ type: string
+ description: Descripción detallada del error.
+ example: Código inválido para el canal
+
+paths:
+ /transactions:
+ post:
+ tags:
+ - Transaction
+ summary: Crea una nueva transacción
+ operationId: createTransaction
+ security:
+ - bearerAuth: []
+ parameters:
+ - $ref: '#/components/parameters/Request-ID'
+ - $ref: '#/components/parameters/request-date'
+ - $ref: '#/components/parameters/caller-name'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TransactionRequest'
+ responses:
+ '201':
+ description: Transacción creada exitosamente.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/TransactionResponse'
+ '400':
+ description: Solicitud inválida.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiException'
+ '401':
+ description: No autorizado.
+ '403':
+ description: Prohibido.
+ '404':
+ description: No encontrado.
+ '429':
+ description: Demasiadas solicitudes.
+ '500':
+ description: Error interno del servidor.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiException'
+ '503':
+ description: Servicio no disponible.
+
+ /transactions/{transactionExternalId}:
+ get:
+ tags:
+ - Transaction
+ summary: Recupera una transacción por su ID externo
+ operationId: getTransaction
+ security:
+ - bearerAuth: []
+ parameters:
+ - $ref: '#/components/parameters/Request-ID'
+ - $ref: '#/components/parameters/request-date'
+ - $ref: '#/components/parameters/caller-name'
+ - name: transactionExternalId
+ in: path
+ required: true
+ schema:
+ type: string
+ format: uuid
+ description: ID externo de la transacción.
+ responses:
+ '200':
+ description: Transacción recuperada exitosamente.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GetTransactionResponse'
+ '400':
+ description: Solicitud inválida.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiException'
+ '401':
+ description: No autorizado.
+ '403':
+ description: Prohibido.
+ '404':
+ description: No encontrado.
+ '429':
+ description: Demasiadas solicitudes.
+ '500':
+ description: Error interno del servidor.
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ApiException'
+ '503':
+ description: Servicio no disponible.
+
+security:
+ - bearerAuth: []
diff --git a/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/ReactiveKafkaConsumerTest.java b/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/ReactiveKafkaConsumerTest.java
new file mode 100644
index 0000000..d7462b0
--- /dev/null
+++ b/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/ReactiveKafkaConsumerTest.java
@@ -0,0 +1,127 @@
+package com.bcp.services.transaction.business;
+
+import com.bcp.services.transaction.model.AnalysisResult;
+import com.bcp.services.transaction.trx.model.TransactionResponse;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.common.TopicPartition;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.kafka.receiver.KafkaReceiver;
+import reactor.kafka.receiver.ReceiverOffset;
+import reactor.kafka.receiver.ReceiverRecord;
+
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class ReactiveKafkaConsumerTest {
+
+ @Mock
+ private KafkaReceiver kafkaReceiver;
+
+ @Mock
+ private TransactionService transactionService;
+
+ @InjectMocks
+ private ReactiveKafkaConsumer reactiveKafkaConsumer;
+
+ @Mock
+ private ObjectMapper objectMapper;
+
+ @Captor
+ private ArgumentCaptor> classCaptor;
+
+ @Test
+ @DisplayName("Initialize successfully")
+ void initializeSuccessfully() throws JsonProcessingException {
+ ConsumerRecord consumerRecord = new ConsumerRecord<>(
+ "test-topic",
+ 0,
+ 0L,
+ "9295f66a-6db4-4a39-9af2-2c524f49fb39",
+ "{\"transactionId\":\"4e375479-048e-43d5-ae02-6612a03e9037\",\"isFraudulent\":false}");
+
+ ReceiverRecord mockRecord = getStringStringReceiverRecord(consumerRecord);
+ doReturn(Flux.just(mockRecord)).when(kafkaReceiver).receive();
+
+ when(objectMapper.readValue(anyString(), classCaptor.capture()))
+ .thenReturn(AnalysisResult.builder()
+ .transactionId("4e375479-048e-43d5-ae02-6612a03e9037")
+ .isFraudulent(false)
+ .build());
+
+ var transactionResponse = new TransactionResponse()
+ .transactionExternalId(UUID.fromString("9295f66a-6db4-4a39-9af2-2c524f49fb39"));
+ doReturn(Mono.just(transactionResponse)).when(transactionService).updateTrx(any());
+
+ reactiveKafkaConsumer.initialize();
+
+ verify(transactionService, times(1)).updateTrx(any());
+ }
+
+ @Test
+ @DisplayName("Fail to initialize due to Kafka error")
+ void failToInitializeDueToKafkaError() throws JsonProcessingException {
+ ConsumerRecord consumerRecord = new ConsumerRecord<>(
+ "test-topic",
+ 0,
+ 0L,
+ "9295f66a-6db4-4a39-9af2-2c524f49fb39",
+ "{\"transactionId0\":\"4e375479-048e-43d5-ae02-6612a03e9037\",\"isFraudulent0\":false}");
+
+ ReceiverRecord mockRecord = getStringStringReceiverRecord(consumerRecord);
+ doReturn(Flux.just(mockRecord)).when(kafkaReceiver).receive();
+
+ when(objectMapper.readValue(anyString(), classCaptor.capture()))
+ .thenThrow(new RuntimeException("Error parsing JSON"));
+
+ reactiveKafkaConsumer.initialize();
+
+ verify(transactionService, never()).updateTrx(any());
+ }
+
+ private static ReceiverRecord getStringStringReceiverRecord(
+ ConsumerRecord consumerRecord) {
+ ReceiverOffset receiverOffset = new ReceiverOffset() {
+
+ @Override
+ public TopicPartition topicPartition() {
+ return new TopicPartition("test-topic", 0);
+ }
+
+ @Override
+ public long offset() {
+ return 0;
+ }
+
+ @Override
+ public void acknowledge() {
+ // Acknowledge logic
+ }
+
+ @Override
+ public Mono commit() {
+ return Mono.empty();
+ }
+ };
+
+ return new ReceiverRecord<>(consumerRecord, receiverOffset);
+ }
+}
\ No newline at end of file
diff --git a/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/ReactiveKafkaProducerTest.java b/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/ReactiveKafkaProducerTest.java
new file mode 100644
index 0000000..06a70bb
--- /dev/null
+++ b/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/ReactiveKafkaProducerTest.java
@@ -0,0 +1,56 @@
+package com.bcp.services.transaction.business;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.test.annotation.DirtiesContext;
+import reactor.core.publisher.Mono;
+import reactor.core.publisher.Flux;
+import reactor.test.StepVerifier;
+import reactor.kafka.sender.KafkaSender;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doReturn;
+
+@ExtendWith(MockitoExtension.class)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
+@TestInstance(TestInstance.Lifecycle.PER_METHOD)
+class ReactiveKafkaProducerTest {
+
+ @Mock
+ private KafkaSender kafkaSender;
+
+ @InjectMocks
+ private ReactiveKafkaProducer reactiveKafkaProducer;
+
+ @Test
+ @DisplayName("Send message successfully")
+ void sendMessageSuccessfully() {
+ // Arrange
+ doReturn(Flux.empty()).when(kafkaSender).send(any(Mono.class));
+
+ // Act
+ Mono result = reactiveKafkaProducer.sendMessage("test-topic", "test-key", "test-value");
+
+ // Assert
+ StepVerifier.create(result)
+ .expectComplete()
+ .verify();
+ }
+
+ @Test
+ @DisplayName("Fail to send message")
+ void failToSendMessage() {
+ doReturn(Flux.error(new RuntimeException("Kafka send error"))).when(kafkaSender).send(any(Mono.class));
+
+ Mono result = reactiveKafkaProducer.sendMessage("test-topic", "test-key", "test-value");
+
+ StepVerifier.create(result)
+ .expectComplete()
+ .verify();
+ }
+}
\ No newline at end of file
diff --git a/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/impl/GetTransactionServiceImplTest.java b/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/impl/GetTransactionServiceImplTest.java
new file mode 100644
index 0000000..bef7928
--- /dev/null
+++ b/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/impl/GetTransactionServiceImplTest.java
@@ -0,0 +1,116 @@
+package com.bcp.services.transaction.business.impl;
+
+import com.bcp.services.transaction.config.ApplicationProperties;
+import com.bcp.services.transaction.config.core.utils.utils.PropertyUtils;
+import com.bcp.services.transaction.model.TransactionEntity;
+import com.bcp.services.transaction.repository.TransactionRepository;
+import com.bcp.services.transaction.trx.model.GetTransactionResponse;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.env.Environment;
+import org.springframework.jdbc.BadSqlGrammarException;
+import org.springframework.test.annotation.DirtiesContext;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.math.BigDecimal;
+import java.sql.SQLDataException;
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD)
+@TestInstance(TestInstance.Lifecycle.PER_METHOD)
+class GetTransactionServiceImplTest {
+
+ @Mock
+ private Environment environment;
+
+ @Mock
+ private TransactionRepository transactionRepository;
+
+ @Mock
+ private ApplicationProperties applicationProperties;
+
+ @InjectMocks
+ private TransactionServiceImpl transactionService;
+
+ private UUID transactionExternalId;
+ private TransactionEntity transactionEntity;
+
+ @BeforeEach
+ void setUp() {
+ transactionExternalId = UUID.randomUUID();
+ transactionEntity = TransactionEntity.builder()
+ .id(transactionExternalId.toString())
+ .status("APPROVED")
+ .transactionTypeCode("220")
+ .amount(new BigDecimal("100.00"))
+ .createdAt(LocalDateTime.parse("2021-08-01T00:00:00"))
+ .updatedAt(LocalDateTime.parse("2021-08-01T00:00:00"))
+ .build();
+ }
+
+ @Test
+ @DisplayName("Return transaction when transaction is found")
+ void returnTransactionWhenTransactionIsFound() {
+
+ when(applicationProperties.getTrxTypes()).thenReturn(Map.of(
+ "220", "Transferencias ordinaria",
+ "225", "Pago a cta tarjeta",
+ "320", "ORDINARIAS - INMEDIATAS",
+ "325", "PAGO DE TARJETA DE CREDITO - INMEDIATAS"));
+ when(transactionRepository.findById(anyString())).thenReturn(Mono.just(transactionEntity));
+
+ Mono result = transactionService.getTrx(transactionExternalId);
+
+ StepVerifier.create(result)
+ .expectNextMatches(response -> response.getTransactionExternalId().equals(transactionExternalId))
+ .verifyComplete();
+ }
+
+ @Test
+ @DisplayName("Throw ApiException when error occurs while obtaining transaction")
+ void throwApiExceptionWhenErrorOccursWhileObtainingTransaction() {
+
+ PropertyUtils.setResolver(environment);
+
+ given(environment.getProperty("spring.application.name")).willReturn("business-transaction");
+ given(environment.getProperty("application.api.error-code.external-error.code")).willReturn("T0099");
+ given(environment.getProperty("application.api.error-code.external-error.description")).willReturn(
+ "Internal server error");
+ given(environment.getProperty("application.api.error-code.external-error.error-type")).willReturn("Technical");
+
+ var exception = new BadSqlGrammarException("Database error", "SQL", new SQLDataException("Database error"));
+ when(transactionRepository.findById(anyString())).thenReturn(Mono.error(exception));
+
+ Mono result = transactionService.getTrx(transactionExternalId);
+
+ StepVerifier.create(result)
+ .expectErrorMatches(throwable -> throwable.getMessage().contains("Internal server error"))
+ .verify();
+ }
+
+ @Test
+ @DisplayName("Return empty when transaction is not found")
+ void returnEmptyWhenTransactionIsNotFound() {
+ when(transactionRepository.findById(anyString())).thenReturn(Mono.empty());
+
+ Mono result = transactionService.getTrx(transactionExternalId);
+
+ StepVerifier.create(result)
+ .expectNextCount(0)
+ .verifyComplete();
+ }
+}
diff --git a/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/impl/SaveTransactionServiceImplTest.java b/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/impl/SaveTransactionServiceImplTest.java
new file mode 100644
index 0000000..5d8b5cc
--- /dev/null
+++ b/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/impl/SaveTransactionServiceImplTest.java
@@ -0,0 +1,238 @@
+package com.bcp.services.transaction.business.impl;
+
+import com.bcp.services.transaction.business.ReactiveKafkaProducer;
+import com.bcp.services.transaction.config.ApplicationProperties;
+import com.bcp.services.transaction.config.core.utils.exception.ApiException;
+import com.bcp.services.transaction.config.core.utils.utils.PropertyUtils;
+import com.bcp.services.transaction.model.TransactionEntity;
+import com.bcp.services.transaction.trx.model.TransactionRequest;
+import com.bcp.services.transaction.trx.model.TransactionResponse;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+import org.springframework.core.env.Environment;
+import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
+import org.springframework.data.r2dbc.core.ReactiveInsertOperation;
+import org.springframework.test.util.ReflectionTestUtils;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class SaveTransactionServiceImplTest {
+
+ public static final ApplicationProperties.TrxStatuses TRX_STATUSES = ApplicationProperties.TrxStatuses.builder()
+ .pending("PENDING")
+ .approved("APPROVED")
+ .rejected("REJECTED")
+ .build();
+ @Mock
+ private Environment environment;
+
+ @Mock
+ private R2dbcEntityTemplate entityTemplate;
+
+ @Mock
+ private ReactiveKafkaProducer reactiveKafkaProducer;
+
+ @Mock
+ private ApplicationProperties applicationProperties;
+
+ @InjectMocks
+ private TransactionServiceImpl transactionService;
+
+ private TransactionRequest transactionRequest;
+ private TransactionEntity transactionEntity;
+
+ @Mock
+ private ReactiveInsertOperation.ReactiveInsert insertMock;
+
+ @Captor
+ private ArgumentCaptor topicCaptor;
+
+ @Captor
+ private ArgumentCaptor keyCaptor;
+
+ @Captor
+ private ArgumentCaptor messageCaptor;
+
+ @Mock
+ private ObjectMapper objectMapper;
+
+ @BeforeEach
+ void setUp() {
+
+ transactionRequest = new TransactionRequest()
+ .accountExternalIdDebit(UUID.randomUUID())
+ .accountExternalIdCredit(UUID.randomUUID())
+ .transactionTypeCode("220")
+ .amount(new BigDecimal("100.00"));
+
+ transactionEntity = TransactionEntity.builder()
+ .id(UUID.randomUUID().toString())
+ .accountExternalIdDebit(transactionRequest.getAccountExternalIdDebit().toString())
+ .accountExternalIdCredit(transactionRequest.getAccountExternalIdCredit().toString())
+ .transactionTypeCode(transactionRequest.getTransactionTypeCode())
+ .amount(transactionRequest.getAmount())
+ .status("PENDING")
+ .createdAt(LocalDateTime.now())
+ .build();
+
+ ReflectionTestUtils.setField(transactionService, "topic", "transaction-anti-fraud-validation");
+
+ when(applicationProperties.getTrxStatuses()).thenReturn(TRX_STATUSES);
+
+
+ }
+
+ @Test
+ @DisplayName("Return transaction when it is saved and sent for anti-fraud validation")
+ void returnTransactionWhenItIsSavedAndSentForAntiFraudValidation() throws JSONException, JsonProcessingException {
+
+ when(entityTemplate.insert(TransactionEntity.class)).thenReturn(insertMock);
+ when(insertMock.using(any(TransactionEntity.class))).thenReturn(Mono.just(transactionEntity));
+ when(objectMapper.writeValueAsString(any())).thenReturn("{\"id\":null,\"amount\":100.00,\"currency\":null}");
+ when(reactiveKafkaProducer.sendMessage(any(), any(), any())).thenReturn(Mono.empty());
+
+ Mono result = transactionService.createTrx(Mono.just(transactionRequest));
+
+ StepVerifier.create(result)
+ .expectNextMatches(response -> response.getTransactionExternalId()
+ .equals(UUID.fromString(transactionEntity.getId())))
+ .verifyComplete();
+
+ verify(reactiveKafkaProducer).sendMessage(topicCaptor.capture(), keyCaptor.capture(), messageCaptor.capture());
+
+ assertEquals("transaction-anti-fraud-validation", topicCaptor.getValue());
+ assertEquals(36, keyCaptor.getValue().length());
+
+ JSONObject expectedJson = new JSONObject("{\"id\":null,\"amount\":100.00,\"currency\":null}");
+ JSONObject actualJson = new JSONObject(messageCaptor.getValue());
+
+ expectedJson.remove("id");
+ actualJson.remove("id");
+
+ JSONAssert.assertEquals(expectedJson, actualJson, JSONCompareMode.LENIENT);
+
+ verify(reactiveKafkaProducer, times(1)).sendMessage(any(), any(), any());
+ }
+
+ @Test
+ @DisplayName("Return transaction when it is saved and an error occurs while sending it for anti-fraud validation")
+ void returnTransactionWhenItIsSavedAndAnErrorOccursWhileSendingItForAntiFraudValidation() throws JsonProcessingException {
+
+ when(entityTemplate.insert(TransactionEntity.class)).thenReturn(insertMock);
+ when(insertMock.using(any(TransactionEntity.class))).thenReturn(Mono.just(transactionEntity));
+ when(objectMapper.writeValueAsString(any())).thenReturn("{\"id\":null,\"amount\":100.00,\"currency\":null}");
+ doReturn(Mono.error(new RuntimeException("Kafka error")))
+ .when(reactiveKafkaProducer).sendMessage(any(), any(), any());
+
+ Mono result = transactionService.createTrx(Mono.just(transactionRequest));
+
+ StepVerifier.create(result)
+ .expectNextMatches(response -> response.getTransactionExternalId()
+ .equals(UUID.fromString(transactionEntity.getId())))
+ .verifyComplete();
+
+ verify(reactiveKafkaProducer).sendMessage(topicCaptor.capture(), keyCaptor.capture(), messageCaptor.capture());
+
+ assertEquals("transaction-anti-fraud-validation", topicCaptor.getValue());
+ assertEquals(36, keyCaptor.getValue().length());
+
+ verify(reactiveKafkaProducer, times(1)).sendMessage(any(), any(), any());
+
+ }
+
+ @Test
+ @DisplayName("Return transaction when it is saved and a JsonProcessingException occurs")
+ void returnTransactionWhenItIsSavedAndAJsonProcessingExceptionOccurs() throws JsonProcessingException {
+
+ PropertyUtils.setResolver(environment);
+
+ //given(environment.getProperty("spring.application.name")).willReturn("business-transaction");
+ given(environment.getProperty("application.api.error-code.unexpected.code")).willReturn("T0099");
+ given(environment.getProperty("application.api.error-code.unexpected.description")).willReturn(
+ "Internal server error");
+ given(environment.getProperty("application.api.error-code.unexpected.error-type")).willReturn("Technical");
+
+ when(applicationProperties.getTrxStatuses()).thenReturn(ApplicationProperties.TrxStatuses.builder()
+ .pending("PENDING")
+ .approved("APPROVED")
+ .rejected("REJECTED")
+ .build());
+
+ when(entityTemplate.insert(TransactionEntity.class)).thenReturn(insertMock);
+ when(insertMock.using(any(TransactionEntity.class))).thenReturn(Mono.just(transactionEntity));
+ when(objectMapper.writeValueAsString(any())).thenThrow(new JsonProcessingException("Error processing JSON") {
+ });
+
+ Mono result = transactionService.createTrx(Mono.just(transactionRequest));
+
+ StepVerifier.create(result)
+ .expectNextMatches(response -> response.getTransactionExternalId()
+ .equals(UUID.fromString(transactionEntity.getId())))
+ .verifyComplete();
+
+ verify(entityTemplate, times(1)).insert(TransactionEntity.class);
+ verify(insertMock, times(1)).using(any(TransactionEntity.class));
+ verify(objectMapper, times(1)).writeValueAsString(any());
+ verify(reactiveKafkaProducer, times(0)).sendMessage(any(), any(), any());
+ }
+
+ @Test
+ @DisplayName("Throw ApiException when error occurs while saving transaction")
+ void throwApiExceptionWhenErrorOccursWhileSavingTransaction() throws JsonProcessingException {
+
+ PropertyUtils.setResolver(environment);
+
+ //given(environment.getProperty("spring.application.name")).willReturn("business-transaction");
+ given(environment.getProperty("application.api.error-code.external-error.code")).willReturn("T0099");
+ given(environment.getProperty("application.api.error-code.external-error.description")).willReturn(
+ "Internal server error");
+ given(environment.getProperty("application.api.error-code.external-error.error-type")).willReturn("Technical");
+
+ when(entityTemplate.insert(TransactionEntity.class)).thenReturn(insertMock);
+ when(insertMock.using(any(TransactionEntity.class)))
+ .thenReturn(Mono.error(new RuntimeException("Database error")));
+
+ Mono result = transactionService.createTrx(Mono.just(transactionRequest));
+
+ StepVerifier.create(result)
+ .expectErrorMatches(throwable -> {
+ if (throwable instanceof ApiException apiException) {
+ return apiException.getExceptionDetails().get(0).getDescription().contains("Database error");
+ }
+ return false;
+ })
+ .verify();
+
+ verify(entityTemplate, times(1)).insert(TransactionEntity.class);
+ verify(insertMock, times(1)).using(any(TransactionEntity.class));
+ verify(objectMapper, times(0)).writeValueAsString(any());
+ verify(reactiveKafkaProducer, times(0)).sendMessage(any(), any(), any());
+ }
+
+}
diff --git a/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/impl/UpdateTransactionServiceImplTest.java b/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/impl/UpdateTransactionServiceImplTest.java
new file mode 100644
index 0000000..e360f34
--- /dev/null
+++ b/business-transaction-v1/src/test/java/com/bcp/services/transaction/business/impl/UpdateTransactionServiceImplTest.java
@@ -0,0 +1,192 @@
+package com.bcp.services.transaction.business.impl;
+
+import com.bcp.services.transaction.config.ApplicationProperties;
+import com.bcp.services.transaction.config.core.utils.exception.ApiException;
+import com.bcp.services.transaction.config.core.utils.utils.PropertyUtils;
+import com.bcp.services.transaction.model.AnalysisResult;
+import com.bcp.services.transaction.model.TransactionEntity;
+import com.bcp.services.transaction.repository.TransactionRepository;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.core.env.Environment;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class UpdateTransactionServiceImplTest {
+
+ public static final ApplicationProperties.TrxStatuses TRX_STATUSES = ApplicationProperties.TrxStatuses.builder()
+ .pending("PENDING")
+ .approved("APPROVED")
+ .rejected("REJECTED")
+ .build();
+
+ @Mock
+ private Environment environment;
+
+ @Mock
+ private TransactionRepository transactionRepository;
+
+ @Mock
+ private ApplicationProperties applicationProperties;
+
+ @InjectMocks
+ private TransactionServiceImpl transactionService;
+
+ private AnalysisResult analysisResult;
+ private TransactionEntity transactionEntity;
+
+ @BeforeEach
+ void setUp() {
+ analysisResult = AnalysisResult.builder()
+ .transactionId(UUID.randomUUID().toString())
+ .isFraudulent(false)
+ .build();
+
+ transactionEntity = TransactionEntity.builder()
+ .id(analysisResult.getTransactionId())
+ .accountExternalIdDebit(UUID.randomUUID().toString())
+ .accountExternalIdCredit(UUID.randomUUID().toString())
+ .transactionTypeCode("220")
+ .amount(new BigDecimal("100.00"))
+ .status("PENDING")
+ .createdAt(LocalDateTime.now())
+ .build();
+ }
+
+ @Nested
+ class SuccessScenarios {
+
+ @Test
+ @DisplayName("Successfully update transaction when it is not fraudulent, found, and updated")
+ void successfullyUpdateTransactionWhenItIsNotFraudulentFoundAndUpdated() {
+ when(transactionRepository.findById(anyString())).thenReturn(Mono.just(transactionEntity));
+ when(applicationProperties.getTrxStatuses()).thenReturn(TRX_STATUSES);
+ when(transactionRepository.save(any(TransactionEntity.class))).thenReturn(Mono.just(transactionEntity));
+
+ Mono result = transactionService.updateTrx(analysisResult);
+
+ StepVerifier.create(result)
+ .verifyComplete();
+
+ verify(transactionRepository, times(1)).findById(anyString());
+ verify(transactionRepository, times(1)).save(any(TransactionEntity.class));
+ }
+
+ @Test
+ @DisplayName("Successfully update transaction when it is fraudulent, found, and updated")
+ void successfullyUpdateTransactionWhenItIsFraudulentFoundAndUpdated() {
+ analysisResult = AnalysisResult.builder()
+ .transactionId(UUID.randomUUID().toString())
+ .isFraudulent(true)
+ .build();
+
+ when(transactionRepository.findById(anyString())).thenReturn(Mono.just(transactionEntity));
+ when(applicationProperties.getTrxStatuses()).thenReturn(TRX_STATUSES);
+ when(transactionRepository.save(any(TransactionEntity.class))).thenReturn(Mono.just(transactionEntity));
+
+ Mono result = transactionService.updateTrx(analysisResult);
+
+ StepVerifier.create(result)
+ .verifyComplete();
+
+ verify(transactionRepository, times(1)).findById(anyString());
+ verify(transactionRepository, times(1)).save(any(TransactionEntity.class));
+ }
+ }
+
+ @Nested
+ class FailureScenarios {
+
+ @Test
+ @DisplayName("Throw ApiException when transaction not found")
+ void throwApiExceptionWhenTransactionNotFound() {
+
+ PropertyUtils.setResolver(environment);
+
+ //doReturn("business-transaction").when(environment).getProperty("spring.application.name");
+ doReturn("T0099").when(environment).getProperty("application.api.error-code.conflict.code");
+ doReturn("conflict").when(environment).getProperty("application.api.error-code.conflict.description");
+ doReturn("Functional").when(environment).getProperty("application.api.error-code.conflict.error-type");
+
+ when(transactionRepository.findById(anyString())).thenReturn(Mono.empty());
+
+ Mono result = transactionService.updateTrx(analysisResult);
+
+ StepVerifier.create(result)
+ .expectErrorMatches(throwable -> throwable instanceof ApiException)
+ .verify();
+
+ verify(transactionRepository, times(1)).findById(anyString());
+ verify(transactionRepository, times(0)).save(any(TransactionEntity.class));
+ }
+
+ @Test
+ @DisplayName("Throw ApiException when error occurs while finding transaction")
+ void throwApiExceptionWhenErrorOccursWhileFindingTransaction() {
+
+ PropertyUtils.setResolver(environment);
+
+ //doReturn("business-transaction").when(environment).getProperty("spring.application.name");
+ doReturn("T0099").when(environment).getProperty("application.api.error-code.external-error.code");
+ doReturn("Internal server error").when(environment).getProperty("application.api.error-code.external-error.description");
+ doReturn("Technical").when(environment).getProperty("application.api.error-code.external-error.error-type");
+
+ when(transactionRepository.findById(anyString()))
+ .thenReturn(Mono.error(new RuntimeException("Database error")));
+
+ Mono result = transactionService.updateTrx(analysisResult);
+
+ StepVerifier.create(result)
+ .expectErrorMatches(throwable -> throwable instanceof ApiException)
+ .verify();
+
+ verify(transactionRepository, times(1)).findById(anyString());
+ verify(transactionRepository, times(0)).save(any(TransactionEntity.class));
+ }
+
+ @Test
+ @DisplayName("Throw ApiException when error occurs while updating transaction")
+ void throwApiExceptionWhenErrorOccursWhileUpdatingTransaction() {
+
+ PropertyUtils.setResolver(environment);
+
+ //doReturn("business-transaction").when(environment).getProperty("spring.application.name");
+ doReturn("T0099").when(environment).getProperty("application.api.error-code.external-error.code");
+ doReturn("Internal server error").when(environment).getProperty("application.api.error-code.external-error.description");
+ doReturn("Technical").when(environment).getProperty("application.api.error-code.external-error.error-type");
+
+ when(transactionRepository.findById(anyString())).thenReturn(Mono.just(transactionEntity));
+ when(applicationProperties.getTrxStatuses()).thenReturn(TRX_STATUSES);
+ when(transactionRepository.save(any(TransactionEntity.class)))
+ .thenReturn(Mono.error(new RuntimeException("Database error")));
+
+ Mono result = transactionService.updateTrx(analysisResult);
+
+ StepVerifier.create(result)
+ .expectErrorMatches(throwable -> throwable instanceof ApiException)
+ .verify();
+
+ verify(transactionRepository, times(1)).findById(anyString());
+ verify(transactionRepository, times(1)).save(any(TransactionEntity.class));
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/business-transaction-v1/src/test/java/com/bcp/services/transaction/expose/web/TransactionApiImplTest.java b/business-transaction-v1/src/test/java/com/bcp/services/transaction/expose/web/TransactionApiImplTest.java
new file mode 100644
index 0000000..38c7605
--- /dev/null
+++ b/business-transaction-v1/src/test/java/com/bcp/services/transaction/expose/web/TransactionApiImplTest.java
@@ -0,0 +1,98 @@
+package com.bcp.services.transaction.expose.web;
+
+import com.bcp.services.transaction.business.TransactionService;
+import com.bcp.services.transaction.trx.model.GetTransactionResponse;
+import com.bcp.services.transaction.trx.model.TransactionRequest;
+import com.bcp.services.transaction.trx.model.TransactionResponse;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+class TransactionApiImplTest {
+
+ public static final UUID REQUEST_ID = UUID.randomUUID();
+ public static final LocalDateTime REQUEST_DATE = LocalDateTime.now();
+ public static final String CALLER_NAME = "testCaller";
+ @Mock
+ private TransactionService transactionService;
+
+ @InjectMocks
+ private TransactionApiImpl transactionApiImpl;
+
+ @Test
+ @DisplayName("Return http status 201 when transaction is saved")
+ void returnHttpStatus201WhenTransactionIsSaved() {
+ Mono transactionRequest = Mono.just(new TransactionRequest());
+ TransactionResponse transactionResponse = new TransactionResponse();
+
+ when(transactionService.createTrx(any(Mono.class))).thenReturn(Mono.just(transactionResponse));
+
+ Mono> result = transactionApiImpl
+ .createTransaction(REQUEST_ID, REQUEST_DATE, CALLER_NAME, transactionRequest, null);
+
+ StepVerifier.create(result)
+ .assertNext(responseEntity -> {
+ Assertions.assertNotNull(responseEntity);
+ Assertions.assertEquals(HttpStatus.CREATED, responseEntity.getStatusCode());
+ Assertions.assertEquals(transactionResponse, responseEntity.getBody());
+ })
+ .expectComplete()
+ .verify();
+ }
+
+ @Test
+ @DisplayName("Return http status 200 when transaction is found")
+ void returnHttpStatus200WhenTransactionIsFound() {
+ UUID transactionExternalId = UUID.randomUUID();
+ GetTransactionResponse getTransactionResponse = new GetTransactionResponse();
+
+ when(transactionService.getTrx(any(UUID.class))).thenReturn(Mono.just(getTransactionResponse));
+
+ Mono> result = transactionApiImpl
+ .getTransaction(REQUEST_ID, REQUEST_DATE, CALLER_NAME, transactionExternalId, null);
+
+ StepVerifier.create(result)
+ .assertNext(responseEntity -> {
+ Assertions.assertNotNull(responseEntity);
+ Assertions.assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
+ Assertions.assertEquals(getTransactionResponse, responseEntity.getBody());
+ })
+ .expectComplete()
+ .verify();
+ }
+
+ @Test
+ @DisplayName("Return http status 204 when transaction is not found")
+ void returnHttpStatus204WhenTransactionIsNotFound() {
+ UUID transactionExternalId = UUID.randomUUID();
+
+ when(transactionService.getTrx(any(UUID.class))).thenReturn(Mono.empty());
+
+ Mono> result = transactionApiImpl
+ .getTransaction(REQUEST_ID, REQUEST_DATE, CALLER_NAME, transactionExternalId, null);
+
+ StepVerifier.create(result)
+ .assertNext(responseEntity -> {
+ Assertions.assertNotNull(responseEntity);
+ Assertions.assertEquals(HttpStatus.NO_CONTENT, responseEntity.getStatusCode());
+ })
+ .expectComplete()
+ .verify();
+ }
+
+}
\ No newline at end of file