diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dccf9be --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml + +.idea/ diff --git a/docker-compose.yml b/docker-compose.yml index 6e9a9c5..939cc72 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,13 @@ services: environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres + redis: + image: "redis:latest" + ports: + - "6379:6379" + environment: + - ALLOW_EMPTY_PASSWORD=yes + - REDIS_DISABLE_COMMANDS=FLUSHDB,FLUSHALL zookeeper: image: confluentinc/cp-zookeeper:5.5.3 environment: diff --git a/microservices/.gitignore b/microservices/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/microservices/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/microservices/README.md b/microservices/README.md new file mode 100644 index 0000000..2d761c3 --- /dev/null +++ b/microservices/README.md @@ -0,0 +1,37 @@ +## Composición del proyecto +El presente proyecto esta conformado por tres módulos + +``` +1. transaction-gateway +2. transaction-management +3. fraud-evaluation +``` + +## Detalle de Módulos + +* transaction-gateway: Es el orquestador de las funcionalidades hacia el cliente. Utiliza GraphQL +* transaction-management: Microservicio dedicado para el mantenimiento de las transacciones en la BD. +* fraud-evaluation: Microservicio dedicado a la evaluación de fraudes de las transacciones registradas. + +## Pasos a seguir para compilar la aplicación + +1. Ejecutar el comando `docker-compose up -d` en la carpeta raiz del proyecto +2. Crear base de datos **bd_challenge** +3. Ejecutar script **01_init_query.sql** de la carpeta _/script_ +4. Compilar cada uno de los módulos listados previamente + + + +## Detalle de Funcionalidades + +El microservicio transaction-gateway expone las siguientes funcionalidades: + +1. Query + - getAllTransactions(limit, offset) + - getTransactionByCode(transactionCode) + + +2. Mutation + - createTransaction(transactionBody) + + diff --git a/microservices/fraud-evaluation/.gitignore b/microservices/fraud-evaluation/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/microservices/fraud-evaluation/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/microservices/fraud-evaluation/pom.xml b/microservices/fraud-evaluation/pom.xml new file mode 100644 index 0000000..bf2243e --- /dev/null +++ b/microservices/fraud-evaluation/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + com.yape + microservices + 1.0.0-SNAPSHOT + + fraud-evaluation + 1.0.0-SNAPSHOT + fraud-evaluation + Fraud Evaluation Service + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + + org.springframework.kafka + spring-kafka + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + + \ No newline at end of file diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/FraudEvaluationApplication.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/FraudEvaluationApplication.java new file mode 100644 index 0000000..f5c1b1d --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/FraudEvaluationApplication.java @@ -0,0 +1,13 @@ +package com.yape.fraudevaluation; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class FraudEvaluationApplication { + + public static void main(String[] args) { + SpringApplication.run(FraudEvaluationApplication.class, args); + } + +} diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/config/KafkaConfig.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/config/KafkaConfig.java new file mode 100644 index 0000000..0847746 --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/config/KafkaConfig.java @@ -0,0 +1,52 @@ +package com.yape.fraudevaluation.config; + +import com.yape.fraudevaluation.model.dto.TransactionDTO; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.*; +import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer; +import org.springframework.kafka.support.serializer.JsonDeserializer; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String serverUrl; + + @Value("${spring.kafka.consumer.group-id}") + private String groupId; + + @Value("${spring.kafka.consumer.offset-mode}") + private String offsetMode; + + @Bean + public ConsumerFactory consumerFactory() { + Map configProps = new HashMap<>(); + + configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, serverUrl); + configProps.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); + configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class); + configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class); + configProps.put(ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS, StringDeserializer.class); + configProps.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, StringDeserializer.class); + configProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, offsetMode); + + return new DefaultKafkaConsumerFactory<>(configProps, new ErrorHandlingDeserializer<>(), new ErrorHandlingDeserializer<>(new JsonDeserializer<>(TransactionDTO.class))); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + + return factory; + } + +} diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/error/ErrorResponse.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/error/ErrorResponse.java new file mode 100644 index 0000000..11c08ca --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/error/ErrorResponse.java @@ -0,0 +1,13 @@ +package com.yape.fraudevaluation.error; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ErrorResponse { + + private Integer errorCode; + private String description; + +} diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/error/GlobalExceptionHandler.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/error/GlobalExceptionHandler.java new file mode 100644 index 0000000..dcb1cf0 --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/error/GlobalExceptionHandler.java @@ -0,0 +1,23 @@ +package com.yape.fraudevaluation.error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(Throwable.class) + public ResponseEntity handle(Throwable ex) { + log.error("Ocurrió un error en la aplicación {}", ex.toString()); + ex.printStackTrace(); + return new ResponseEntity<>(ErrorResponse.builder() + .errorCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .description(ex.getMessage()) + .build(), HttpStatus.INTERNAL_SERVER_ERROR); + } + +} diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/consumer/FraudMessageConsumer.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/consumer/FraudMessageConsumer.java new file mode 100644 index 0000000..1a7c539 --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/consumer/FraudMessageConsumer.java @@ -0,0 +1,9 @@ +package com.yape.fraudevaluation.kafka.consumer; + +import com.yape.fraudevaluation.model.dto.TransactionDTO; + +public interface FraudMessageConsumer { + + void retrieveMessage(TransactionDTO object); + +} diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/consumer/impl/FraudMessageConsumerImpl.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/consumer/impl/FraudMessageConsumerImpl.java new file mode 100644 index 0000000..045a239 --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/consumer/impl/FraudMessageConsumerImpl.java @@ -0,0 +1,39 @@ +package com.yape.fraudevaluation.kafka.consumer.impl; + +import com.yape.fraudevaluation.kafka.consumer.FraudMessageConsumer; +import com.yape.fraudevaluation.model.dto.TransactionDTO; +import com.yape.fraudevaluation.service.FraudEvaluationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class FraudMessageConsumerImpl implements FraudMessageConsumer { + + @Value("${fraud-evaluation.kafka-topics.transaction}") + private String transactionTopic; + + private final FraudEvaluationService fraudEvaluationService; + + @KafkaListener( + topics = "${fraud-evaluation.kafka-topics.transaction}", + groupId = "${spring.kafka.consumer.group-id}" + ) + @Override + public void retrieveMessage(TransactionDTO object) { + try { + log.info("Retreived message from Topic {}. Message {}", transactionTopic, object.toString()); + + fraudEvaluationService.evaluateTransaction(object) + .subscribe(); + } catch (Exception e) { + log.error("Exception on message send {}", e.toString()); + throw new RuntimeException("Error occurs message retrieve"); + } + } + +} diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/producer/FraudMessageProducer.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/producer/FraudMessageProducer.java new file mode 100644 index 0000000..721984a --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/producer/FraudMessageProducer.java @@ -0,0 +1,9 @@ +package com.yape.fraudevaluation.kafka.producer; + +import com.yape.fraudevaluation.model.dto.TransactionDTO; + +public interface FraudMessageProducer { + + boolean sendMessage(TransactionDTO transactionDTO); + +} diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/producer/impl/FraudMessageProducerImpl.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/producer/impl/FraudMessageProducerImpl.java new file mode 100644 index 0000000..4ba7aa1 --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/kafka/producer/impl/FraudMessageProducerImpl.java @@ -0,0 +1,35 @@ +package com.yape.fraudevaluation.kafka.producer.impl; + +import com.yape.fraudevaluation.kafka.producer.FraudMessageProducer; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.yape.fraudevaluation.model.dto.TransactionDTO; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class FraudMessageProducerImpl implements FraudMessageProducer { + + private final KafkaTemplate kafkaTemplate; + + @Value("${fraud-evaluation.kafka-topics.fraud-evaluation}") + private String fraudTopic; + + @Override + public boolean sendMessage(TransactionDTO transactionDTO) { + try { + log.info("Sending message {} to Topic {}", transactionDTO.toString(), fraudTopic); + + kafkaTemplate.send(fraudTopic, transactionDTO); + + return true; + } catch (Exception e) { + log.error("Exception on message send {}", e.toString()); + throw new RuntimeException("Error occurs on message send"); + } + } + +} diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/model/constant/TransactionStatusEnum.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/model/constant/TransactionStatusEnum.java new file mode 100644 index 0000000..6a3305c --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/model/constant/TransactionStatusEnum.java @@ -0,0 +1,18 @@ +package com.yape.fraudevaluation.model.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum TransactionStatusEnum { + + PENDING(1, "PENDING"), + APPROBED(2, "APPROBED"), + REJECTED(3, "REJECTED") + ; + + private final Integer value; + private final String description; + +} diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/model/dto/TransactionDTO.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/model/dto/TransactionDTO.java new file mode 100644 index 0000000..417bb43 --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/model/dto/TransactionDTO.java @@ -0,0 +1,22 @@ +package com.yape.fraudevaluation.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransactionDTO implements Serializable { + + private Long transactionId; + private String transactionCode; + private String transactionState; + private BigDecimal transactionValue; + +} diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/service/FraudEvaluationService.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/service/FraudEvaluationService.java new file mode 100644 index 0000000..1d9b5ee --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/service/FraudEvaluationService.java @@ -0,0 +1,10 @@ +package com.yape.fraudevaluation.service; + +import com.yape.fraudevaluation.model.dto.TransactionDTO; +import reactor.core.publisher.Mono; + +public interface FraudEvaluationService { + + Mono evaluateTransaction(TransactionDTO transactionDTO); + +} diff --git a/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/service/impl/FraudEvaluationServiceImpl.java b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/service/impl/FraudEvaluationServiceImpl.java new file mode 100644 index 0000000..8893c1e --- /dev/null +++ b/microservices/fraud-evaluation/src/main/java/com/yape/fraudevaluation/service/impl/FraudEvaluationServiceImpl.java @@ -0,0 +1,40 @@ +package com.yape.fraudevaluation.service.impl; + +import com.yape.fraudevaluation.kafka.producer.FraudMessageProducer; +import com.yape.fraudevaluation.model.constant.TransactionStatusEnum; +import com.yape.fraudevaluation.model.dto.TransactionDTO; +import com.yape.fraudevaluation.service.FraudEvaluationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.math.BigDecimal; + +@Slf4j +@Service +@RequiredArgsConstructor +public class FraudEvaluationServiceImpl implements FraudEvaluationService { + + @Value("${fraud-evaluation.value-limit}") + private BigDecimal valueLimit; + + private final FraudMessageProducer fraudMessageProducer; + + @Override + public Mono evaluateTransaction(TransactionDTO transactionDTO) { + return Mono.just(transactionDTO) + .filter(transaction -> TransactionStatusEnum.PENDING.getDescription().equals(transactionDTO.getTransactionState())) + .doOnNext(this::evaluateValue) + .flatMap(transaction -> Mono.fromCallable(() -> fraudMessageProducer.sendMessage(transaction)) + .then()); + } + + private void evaluateValue(TransactionDTO transaction) { + transaction.setTransactionState((transaction.getTransactionValue().compareTo(valueLimit) > 0) + ? TransactionStatusEnum.REJECTED.getDescription() + : TransactionStatusEnum.APPROBED.getDescription()); + } + +} diff --git a/microservices/fraud-evaluation/src/main/resources/application.yml b/microservices/fraud-evaluation/src/main/resources/application.yml new file mode 100644 index 0000000..7f8f907 --- /dev/null +++ b/microservices/fraud-evaluation/src/main/resources/application.yml @@ -0,0 +1,37 @@ +application: + core: + api: + path: /api/v1.0/fraud-evaluation + +server: + port: 8094 + address: + compression: + mime-types: + +spring: + kafka: + bootstrap-servers: 'localhost:9092' + consumer: + group-id: yape-group + offset-mode: 'earliest' + properties: + spring: + json: + trusted: + packages: com.yape.transactionmanagement.model.dto + producer: + properties: + spring: + json: + add: + type: + headers: false + key-serializer: org.springframework.kafka.support.serializer.JsonSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + +fraud-evaluation: + kafka-topics: + fraud-evaluation: 'topic-fraud' + transaction: 'topic-transaction' + value-limit: 1000 \ No newline at end of file diff --git a/microservices/pom.xml b/microservices/pom.xml new file mode 100644 index 0000000..738a515 --- /dev/null +++ b/microservices/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.3.0 + + + + com.yape + microservices + 1.0.0-SNAPSHOT + pom + + + fraud-evaluation + transaction-management + transaction-gateway + + + \ No newline at end of file diff --git a/microservices/transaction-gateway/.gitignore b/microservices/transaction-gateway/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/microservices/transaction-gateway/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/microservices/transaction-gateway/pom.xml b/microservices/transaction-gateway/pom.xml new file mode 100644 index 0000000..3a508e2 --- /dev/null +++ b/microservices/transaction-gateway/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + com.yape + microservices + 1.0.0-SNAPSHOT + + transaction-gateway + 1.0.0-SNAPSHOT + transaction-gateway + Transaction Gateway Service + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + + org.springframework.boot + spring-boot-starter-graphql + + + + com.graphql-java + graphql-java-extended-scalars + 22.0 + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + \ No newline at end of file diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/TransactionGatewayApplication.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/TransactionGatewayApplication.java new file mode 100644 index 0000000..18f07d1 --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/TransactionGatewayApplication.java @@ -0,0 +1,13 @@ +package com.yape.transactiongateway; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TransactionGatewayApplication { + + public static void main(String[] args) { + SpringApplication.run(TransactionGatewayApplication.class, args); + } + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/config/GraphQLConfiguration.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/config/GraphQLConfiguration.java new file mode 100644 index 0000000..abbb652 --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/config/GraphQLConfiguration.java @@ -0,0 +1,17 @@ +package com.yape.transactiongateway.config; + +import graphql.scalars.ExtendedScalars; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.graphql.execution.RuntimeWiringConfigurer; + +@Configuration +public class GraphQLConfiguration { + + @Bean + public RuntimeWiringConfigurer runtimeWiringConfigurer() { + return wiringBuilder -> wiringBuilder + .scalar(ExtendedScalars.GraphQLBigDecimal); + } + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/config/client/TransactionRestClient.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/config/client/TransactionRestClient.java new file mode 100644 index 0000000..8811c6c --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/config/client/TransactionRestClient.java @@ -0,0 +1,16 @@ +package com.yape.transactiongateway.config.client; + +import com.yape.transactiongateway.model.dto.RegisterTransactionRequest; +import com.yape.transactiongateway.model.dto.TransactionResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface TransactionRestClient { + + Flux getAllTransactions(); + + Mono getTransactionByCode(String transactionCode); + + Mono registerTransaction(RegisterTransactionRequest request); + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/config/client/impl/TransactionRestClientImpl.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/config/client/impl/TransactionRestClientImpl.java new file mode 100644 index 0000000..a9644cc --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/config/client/impl/TransactionRestClientImpl.java @@ -0,0 +1,89 @@ +package com.yape.transactiongateway.config.client.impl; + +import com.yape.transactiongateway.config.client.TransactionRestClient; +import com.yape.transactiongateway.model.dto.RegisterTransactionRequest; +import com.yape.transactiongateway.model.dto.TransactionResponse; +import io.netty.channel.ChannelOption; +import io.netty.handler.timeout.ReadTimeoutHandler; +import io.netty.handler.timeout.WriteTimeoutHandler; +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Component +public class TransactionRestClientImpl implements TransactionRestClient { + + private WebClient restClient; + + @Value("${http-client.transaction-management.base-url}") + private String baseUrl; + + @Value("${http-client.transaction-management.read-timeout}") + private long readTimeout; + + @Value("${http-client.transaction-management.write-timeout}") + private long writeTimeout; + + @PostConstruct + public void init() { + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, ((Long) writeTimeout).intValue()) + .responseTimeout(Duration.ofMillis(readTimeout)) + .doOnConnected(conn -> + conn.addHandlerLast(new ReadTimeoutHandler(readTimeout, TimeUnit.MILLISECONDS)) + .addHandlerLast(new WriteTimeoutHandler(writeTimeout, TimeUnit.MILLISECONDS))); + + this.restClient = WebClient.builder() + .baseUrl(baseUrl) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .build(); + } + + @Override + public Flux getAllTransactions() { + log.info("Begin HTTP Call [GET] transactions"); + return restClient.get() + .uri("/transactions") + .retrieve() + .bodyToFlux(TransactionResponse.class) + .doAfterTerminate(() -> log.info("OK <-- Respuesta de Éxito en la petición HTTP")) + .doOnError(err -> log.error("Error <-- Ocurrió un error en la petición HTTP. Msg {}", err.toString())) + ; + } + + @Override + public Mono getTransactionByCode(String transactionCode) { + log.info("Begin HTTP Call [GET] transactions/{txCode}"); + return restClient.get() + .uri("/transactions/{transactionCode}", transactionCode) + .retrieve() + .bodyToMono(TransactionResponse.class) + .doOnSuccess(success -> log.info("OK <-- Respuesta de Éxito en la petición HTTP")) + .doOnError(err -> log.error("Error <-- Ocurrió un error en la petición HTTP. Msg {}", err.toString())) + ; + } + + @Override + public Mono registerTransaction(RegisterTransactionRequest request) { + log.info("Begin HTTP Call [POST] transactions with request {}", request.toString()); + return restClient.post() + .uri("/transactions") + .bodyValue(request) + .retrieve() + .bodyToMono(Void.class) + .doOnSuccess(success -> log.info("OK <-- Respuesta de Éxito en la petición HTTP")) + .doOnError(err -> log.error("Error <-- Ocurrió un error en la petición HTTP. Msg {}", err.toString())) + ; + } + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/error/ErrorResponse.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/error/ErrorResponse.java new file mode 100644 index 0000000..43c4d3f --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/error/ErrorResponse.java @@ -0,0 +1,13 @@ +package com.yape.transactiongateway.error; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ErrorResponse { + + private Integer errorCode; + private String description; + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/error/GlobalExceptionHandler.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/error/GlobalExceptionHandler.java new file mode 100644 index 0000000..cc46f9c --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/error/GlobalExceptionHandler.java @@ -0,0 +1,27 @@ +package com.yape.transactiongateway.error; + +import graphql.GraphQLError; +import graphql.GraphqlErrorBuilder; +import graphql.schema.DataFetchingEnvironment; +import lombok.extern.slf4j.Slf4j; +import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter; +import org.springframework.graphql.execution.ErrorType; +import org.springframework.web.bind.annotation.ControllerAdvice; + +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler extends DataFetcherExceptionResolverAdapter { + + @Override + protected GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) { + log.error("Ocurrió un error en la aplicación {}", ex.toString()); + ex.printStackTrace(); + return GraphqlErrorBuilder.newError() + .errorType(ErrorType.INTERNAL_ERROR) + .message(ex.getMessage()) + .path(env.getExecutionStepInfo().getPath()) + .location(env.getField().getSourceLocation()) + .build(); + } + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/common/TransactionState.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/common/TransactionState.java new file mode 100644 index 0000000..7014386 --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/common/TransactionState.java @@ -0,0 +1,16 @@ +package com.yape.transactiongateway.model.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransactionState { + + private String name; + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/common/TransactionType.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/common/TransactionType.java new file mode 100644 index 0000000..2808053 --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/common/TransactionType.java @@ -0,0 +1,16 @@ +package com.yape.transactiongateway.model.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransactionType { + + private String name; + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/dto/RegisterTransactionRequest.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/dto/RegisterTransactionRequest.java new file mode 100644 index 0000000..b6ea96c --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/dto/RegisterTransactionRequest.java @@ -0,0 +1,19 @@ +package com.yape.transactiongateway.model.dto; + +import lombok.*; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@ToString +public class RegisterTransactionRequest { + + private String accountExternalIdDebit; + private String accountExternalIdCredit; + private Integer tranferTypeId; + private BigDecimal value; + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/dto/TransactionResponse.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/dto/TransactionResponse.java new file mode 100644 index 0000000..5d8a6f6 --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/model/dto/TransactionResponse.java @@ -0,0 +1,25 @@ +package com.yape.transactiongateway.model.dto; + +import com.yape.transactiongateway.model.common.TransactionState; +import com.yape.transactiongateway.model.common.TransactionType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransactionResponse implements Serializable { + + private String transactionExternalId; + private TransactionType transactionType; + private TransactionState transactionStatus; + private BigDecimal value; + private String createdAt; + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/service/TransactionService.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/service/TransactionService.java new file mode 100644 index 0000000..8e708fa --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/service/TransactionService.java @@ -0,0 +1,16 @@ +package com.yape.transactiongateway.service; + +import com.yape.transactiongateway.model.dto.RegisterTransactionRequest; +import com.yape.transactiongateway.model.dto.TransactionResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface TransactionService { + + Flux getAllTransactions(Integer limit, Integer offset); + + Mono getTransactionByCode(String transactionCode); + + Mono createTransaction(RegisterTransactionRequest transactionBody); + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/service/impl/TransactionServiceImpl.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/service/impl/TransactionServiceImpl.java new file mode 100644 index 0000000..29b562a --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/service/impl/TransactionServiceImpl.java @@ -0,0 +1,41 @@ +package com.yape.transactiongateway.service.impl; + +import com.yape.transactiongateway.config.client.TransactionRestClient; +import com.yape.transactiongateway.model.dto.RegisterTransactionRequest; +import com.yape.transactiongateway.model.dto.TransactionResponse; +import com.yape.transactiongateway.service.TransactionService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Objects; + +@Service +@RequiredArgsConstructor +public class TransactionServiceImpl implements TransactionService { + + private final TransactionRestClient transactionRestClient; + + @Override + public Flux getAllTransactions(Integer limit, Integer offset) { + Flux allTransactions = transactionRestClient.getAllTransactions(); + + if (Objects.nonNull(offset)) allTransactions = allTransactions.skip(offset); + + if (Objects.nonNull(limit)) allTransactions = allTransactions.take(limit); + + return allTransactions; + } + + @Override + public Mono getTransactionByCode(String transactionCode) { + return transactionRestClient.getTransactionByCode(transactionCode); + } + + @Override + public Mono createTransaction(RegisterTransactionRequest transactionBody) { + return transactionRestClient.registerTransaction(transactionBody); + } + +} diff --git a/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/web/TransactionGraphController.java b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/web/TransactionGraphController.java new file mode 100644 index 0000000..c08abf8 --- /dev/null +++ b/microservices/transaction-gateway/src/main/java/com/yape/transactiongateway/web/TransactionGraphController.java @@ -0,0 +1,40 @@ +package com.yape.transactiongateway.web; + +import com.yape.transactiongateway.model.dto.RegisterTransactionRequest; +import com.yape.transactiongateway.model.dto.TransactionResponse; +import com.yape.transactiongateway.service.TransactionService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Slf4j +@Controller +@RequiredArgsConstructor +public class TransactionGraphController { + + private final TransactionService transactionService; + + @QueryMapping + public Flux getAllTransactions(@Argument Integer limit, @Argument Integer offset) { + log.info("Iniciando consulta de transacciones. Limit: {} Offset {}", limit, offset); + return transactionService.getAllTransactions(limit, offset); + } + + @QueryMapping + public Mono getTransactionByCode(@Argument String transactionCode) { + log.info("Iniciando busqueda de la transaccion con codigo {}", transactionCode); + return transactionService.getTransactionByCode(transactionCode); + } + + @MutationMapping + public Mono createTransaction(@Argument RegisterTransactionRequest transactionBody) { + log.info("Creando una nueva transaccion"); + return transactionService.createTransaction(transactionBody); + } + +} diff --git a/microservices/transaction-gateway/src/main/resources/application.yml b/microservices/transaction-gateway/src/main/resources/application.yml new file mode 100644 index 0000000..30e3ef8 --- /dev/null +++ b/microservices/transaction-gateway/src/main/resources/application.yml @@ -0,0 +1,14 @@ +spring: + graphql: + graphiql: + enabled: true + path: '/graphiql' + +server: + port: 8095 + +http-client: + transaction-management: + base-url: 'http://localhost:8093/api/v1.0/transaction-management' + read-timeout: 15000 + write-timeout: 15000 diff --git a/microservices/transaction-gateway/src/main/resources/graphql/schema.graphqls b/microservices/transaction-gateway/src/main/resources/graphql/schema.graphqls new file mode 100644 index 0000000..80d02f7 --- /dev/null +++ b/microservices/transaction-gateway/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,35 @@ +scalar BigDecimal + +type TransactionType { + name: String +} + +type TransactionState { + name: String +} + +type Transaction { + transactionExternalId: String! + transactionType: TransactionType! + transactionStatus: TransactionState! + value: BigDecimal! + createdAt: String! +} + +input RegisterTransactionRequest { + accountExternalIdDebit: String! + accountExternalIdCredit: String! + tranferTypeId: Int! + value: BigDecimal! +} + + + +type Query { + getAllTransactions(limit: Int, offset: Int): [Transaction]! + getTransactionByCode(transactionCode: String): Transaction +} + +type Mutation { + createTransaction(transactionBody: RegisterTransactionRequest): Boolean +} \ No newline at end of file diff --git a/microservices/transaction-management/.gitignore b/microservices/transaction-management/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/microservices/transaction-management/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/microservices/transaction-management/pom.xml b/microservices/transaction-management/pom.xml new file mode 100644 index 0000000..9f323a3 --- /dev/null +++ b/microservices/transaction-management/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + com.yape + microservices + 1.0.0-SNAPSHOT + + transaction-management + 1.0.0-SNAPSHOT + transaction-management + Transaction Management Service + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.postgresql + postgresql + runtime + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + redis.clients + jedis + + + + + + org.springframework.kafka + spring-kafka + + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + \ No newline at end of file diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/TransactionManagementApplication.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/TransactionManagementApplication.java new file mode 100644 index 0000000..ca7b6a6 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/TransactionManagementApplication.java @@ -0,0 +1,13 @@ +package com.yape.transactionmanagement; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TransactionManagementApplication { + + public static void main(String[] args) { + SpringApplication.run(TransactionManagementApplication.class, args); + } + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/builder/TransactionBuilder.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/builder/TransactionBuilder.java new file mode 100644 index 0000000..a06eeb1 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/builder/TransactionBuilder.java @@ -0,0 +1,52 @@ +package com.yape.transactionmanagement.builder; + +import com.yape.transactionmanagement.model.common.TransactionState; +import com.yape.transactionmanagement.model.common.TransactionType; +import com.yape.transactionmanagement.model.constant.TransactionStatusEnum; +import com.yape.transactionmanagement.model.constant.TransactionTypeEnum; +import com.yape.transactionmanagement.model.dto.TransactionDTO; +import com.yape.transactionmanagement.model.request.RegisterTransactionRequest; +import com.yape.transactionmanagement.model.response.TransactionResponse; +import com.yape.transactionmanagement.model.entity.TransactionEntity; +import com.yape.transactionmanagement.util.Utils; + +import java.time.LocalDateTime; +import java.util.UUID; + +public class TransactionBuilder { + + public static TransactionResponse buildTransactionResponse(TransactionEntity entity) { + return TransactionResponse.builder() + .transactionExternalId(entity.getTransactionCode()) + .transactionType(TransactionType.builder() + .name(entity.getTransactionType()).build()) + .transactionStatus(TransactionState.builder() + .name(entity.getTransactionStatus()).build()) + .value(entity.getTransactionValue()) + .createdAt(Utils.formatDateTime(entity.getCreatedAt())) + .build(); + } + + public static TransactionEntity buildTransactionEntity(RegisterTransactionRequest request) { + return TransactionEntity.builder() + .transactionCode(UUID.randomUUID().toString()) + .accountExternalIdDebit(request.getAccountExternalIdDebit()) + .accountExternalIdCredit(request.getAccountExternalIdCredit()) + .transactionType(TransactionTypeEnum.parse(request.getTranferTypeId()).getDescription()) + .transactionStatus(TransactionStatusEnum.PENDING.getDescription()) + .transactionValue(request.getValue()) + .createdAt(LocalDateTime.now()) + .build(); + } + + public static TransactionDTO buildTransactionDTO(TransactionEntity entity) { + return TransactionDTO.builder() + .transactionId(entity.getTransactionId()) + .transactionCode(entity.getTransactionCode()) + .transactionState(entity.getTransactionStatus()) + .transactionValue(entity.getTransactionValue()) + .build(); + } + + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/config/KafkaConfig.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/config/KafkaConfig.java new file mode 100644 index 0000000..35c774c --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/config/KafkaConfig.java @@ -0,0 +1,52 @@ +package com.yape.transactionmanagement.config; + +import com.yape.transactionmanagement.model.dto.TransactionDTO; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.*; +import org.springframework.kafka.support.serializer.ErrorHandlingDeserializer; +import org.springframework.kafka.support.serializer.JsonDeserializer; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String serverUrl; + + @Value("${spring.kafka.consumer.group-id}") + private String groupId; + + @Value("${spring.kafka.consumer.offset-mode}") + private String offsetMode; + + @Bean + public ConsumerFactory consumerFactory() { + Map configProps = new HashMap<>(); + + configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, serverUrl); + configProps.put(ConsumerConfig.GROUP_ID_CONFIG, groupId); + configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class); + configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, ErrorHandlingDeserializer.class); + configProps.put(ErrorHandlingDeserializer.KEY_DESERIALIZER_CLASS, StringDeserializer.class); + configProps.put(ErrorHandlingDeserializer.VALUE_DESERIALIZER_CLASS, StringDeserializer.class); + configProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, offsetMode); + + return new DefaultKafkaConsumerFactory<>(configProps, new ErrorHandlingDeserializer<>(), new ErrorHandlingDeserializer<>(new JsonDeserializer<>(TransactionDTO.class))); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + + return factory; + } + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/config/RedisConfig.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/config/RedisConfig.java new file mode 100644 index 0000000..a7dc864 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/config/RedisConfig.java @@ -0,0 +1,70 @@ +package com.yape.transactionmanagement.config; + +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import redis.clients.jedis.Jedis; + +import java.io.Serializable; +import java.time.Duration; + +@Slf4j +@Configuration +@AutoConfigureAfter(RedisAutoConfiguration.class) +@EnableCaching +public class RedisConfig { + + @Value("${redis.url}") + private String redisUrl; + + @Value("${redis.ttl-secconds}") + private long ttlSeconds; + + @Bean + public RedisTemplate redisCacheTemplate(LettuceConnectionFactory redisConnectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); + template.setConnectionFactory(redisConnectionFactory); + + return template; + } + + @Bean + public CacheManager cacheManager(RedisConnectionFactory factory) { + RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); + + RedisCacheConfiguration redisCacheConfiguration = config + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) + .serializeValuesWith(RedisSerializationContext.SerializationPair + .fromSerializer(new GenericJackson2JsonRedisSerializer())) + .entryTtl(Duration.ofSeconds(ttlSeconds)); + + return RedisCacheManager.builder(factory).cacheDefaults(redisCacheConfiguration) + .build(); + } + + @PostConstruct + public void clearCache() { + log.info("Cleaning all cache entries. Test Only"); + Jedis jedis = new Jedis(redisUrl); + jedis.flushAll(); + jedis.close(); + } + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/error/ErrorResponse.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/error/ErrorResponse.java new file mode 100644 index 0000000..dd29d9f --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/error/ErrorResponse.java @@ -0,0 +1,13 @@ +package com.yape.transactionmanagement.error; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class ErrorResponse { + + private Integer errorCode; + private String description; + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/error/GlobalExceptionHandler.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/error/GlobalExceptionHandler.java new file mode 100644 index 0000000..a837b67 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/error/GlobalExceptionHandler.java @@ -0,0 +1,23 @@ +package com.yape.transactionmanagement.error; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(Throwable.class) + public ResponseEntity handle(Throwable ex) { + log.error("Ocurrió un error en la aplicación {}", ex.toString()); + ex.printStackTrace(); + return new ResponseEntity<>(ErrorResponse.builder() + .errorCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) + .description(ex.getMessage()) + .build(), HttpStatus.INTERNAL_SERVER_ERROR); + } + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/error/NotFoundException.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/error/NotFoundException.java new file mode 100644 index 0000000..0d04388 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/error/NotFoundException.java @@ -0,0 +1,9 @@ +package com.yape.transactionmanagement.error; + +public class NotFoundException extends RuntimeException { + + public NotFoundException(String message) { + super(message); + } + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/consumer/TransactionMessageConsumer.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/consumer/TransactionMessageConsumer.java new file mode 100644 index 0000000..109fe20 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/consumer/TransactionMessageConsumer.java @@ -0,0 +1,9 @@ +package com.yape.transactionmanagement.kafka.consumer; + +import com.yape.transactionmanagement.model.dto.TransactionDTO; + +public interface TransactionMessageConsumer { + + void retrieveMessage(TransactionDTO object); + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/consumer/impl/TransactionMessageConsumerImpl.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/consumer/impl/TransactionMessageConsumerImpl.java new file mode 100644 index 0000000..304aa49 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/consumer/impl/TransactionMessageConsumerImpl.java @@ -0,0 +1,44 @@ +package com.yape.transactionmanagement.kafka.consumer.impl; + +import com.yape.transactionmanagement.kafka.consumer.TransactionMessageConsumer; +import com.yape.transactionmanagement.model.dto.TransactionDTO; +import com.yape.transactionmanagement.repository.TransactionRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Caching; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TransactionMessageConsumerImpl implements TransactionMessageConsumer { + + @Value("${transaction-management.kafka-topics.fraud-evaluation}") + private String fraudTopic; + + private final TransactionRepository transactionRepository; + + @Caching(evict = { + @CacheEvict(value = "transactions", allEntries = true), + @CacheEvict(value = "transaction", key = "#object.transactionId") + }) + @KafkaListener( + topics = "${transaction-management.kafka-topics.fraud-evaluation}", + groupId = "${spring.kafka.consumer.group-id}" + ) + @Override + public void retrieveMessage(TransactionDTO object) { + try { + log.info("Retreived message from Topic {}. Message {}", fraudTopic, object.toString()); + + transactionRepository.updateTransactionState(object.getTransactionId(), object.getTransactionState()); + } catch (Exception e) { + log.error("Exception on message send {}", e.toString()); + throw new RuntimeException("Error occurs message retrieve"); + } + } + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/producer/TransactionMessageProducer.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/producer/TransactionMessageProducer.java new file mode 100644 index 0000000..1611160 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/producer/TransactionMessageProducer.java @@ -0,0 +1,9 @@ +package com.yape.transactionmanagement.kafka.producer; + +import com.yape.transactionmanagement.model.dto.TransactionDTO; + +public interface TransactionMessageProducer { + + boolean sendMessage(TransactionDTO transactionDTO); + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/producer/impl/TransactionMessageProducerImpl.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/producer/impl/TransactionMessageProducerImpl.java new file mode 100644 index 0000000..0e83cc7 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/kafka/producer/impl/TransactionMessageProducerImpl.java @@ -0,0 +1,35 @@ +package com.yape.transactionmanagement.kafka.producer.impl; + +import com.yape.transactionmanagement.kafka.producer.TransactionMessageProducer; +import com.yape.transactionmanagement.model.dto.TransactionDTO; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TransactionMessageProducerImpl implements TransactionMessageProducer { + + private final KafkaTemplate kafkaTemplate; + + @Value("${transaction-management.kafka-topics.transaction}") + private String transactionTopic; + + @Override + public boolean sendMessage(TransactionDTO transactionDTO) { + try { + log.info("Sending message {} to Topic {}", transactionDTO.toString(), transactionTopic); + + kafkaTemplate.send(transactionTopic, transactionDTO); + + return true; + } catch (Exception e) { + log.error("Exception on message send {}", e.toString()); + throw new RuntimeException("Error occurs on message send"); + } + } + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/common/TransactionState.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/common/TransactionState.java new file mode 100644 index 0000000..141d2cb --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/common/TransactionState.java @@ -0,0 +1,16 @@ +package com.yape.transactionmanagement.model.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransactionState { + + private String name; + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/common/TransactionType.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/common/TransactionType.java new file mode 100644 index 0000000..4f5a9ac --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/common/TransactionType.java @@ -0,0 +1,16 @@ +package com.yape.transactionmanagement.model.common; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransactionType { + + private String name; + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/constant/TransactionStatusEnum.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/constant/TransactionStatusEnum.java new file mode 100644 index 0000000..bd6fb54 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/constant/TransactionStatusEnum.java @@ -0,0 +1,19 @@ +package com.yape.transactionmanagement.model.constant; + +import com.yape.transactionmanagement.error.NotFoundException; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum TransactionStatusEnum { + + PENDING(1, "PENDING"), + APPROBED(2, "APPROBED"), + REJECTED(3, "REJECTED") + ; + + private final Integer value; + private final String description; + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/constant/TransactionTypeEnum.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/constant/TransactionTypeEnum.java new file mode 100644 index 0000000..8beefe5 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/constant/TransactionTypeEnum.java @@ -0,0 +1,26 @@ +package com.yape.transactionmanagement.model.constant; + +import com.yape.transactionmanagement.error.NotFoundException; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum TransactionTypeEnum { + + OWN(1, "OWN"), + THIRDPARTY(2, "THIRDPARTY"), + INTERBANK(3, "EXTERNAL") + ; + + private final Integer value; + private final String description; + + public static TransactionTypeEnum parse(Integer value) { + for (TransactionTypeEnum state : TransactionTypeEnum.values()) { + if (value.equals(state.getValue())) return state; + } + throw new NotFoundException("Transaction type not found."); + } + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/dto/TransactionDTO.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/dto/TransactionDTO.java new file mode 100644 index 0000000..e7f74b3 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/dto/TransactionDTO.java @@ -0,0 +1,22 @@ +package com.yape.transactionmanagement.model.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransactionDTO implements Serializable { + + private Long transactionId; + private String transactionCode; + private String transactionState; + private BigDecimal transactionValue; + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/entity/TransactionEntity.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/entity/TransactionEntity.java new file mode 100644 index 0000000..22d2242 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/entity/TransactionEntity.java @@ -0,0 +1,52 @@ +package com.yape.transactionmanagement.model.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity(name = "transaction") +@Table(name = "transaction", schema = "public") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransactionEntity { + + @Id + @Column(name = "id") + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long transactionId; + + @Column(name ="code") + private String transactionCode; + + @Column(name ="account_external_id_debit") + private String accountExternalIdDebit; + + @Column(name ="account_external_id_credit") + private String accountExternalIdCredit; + + @Column(name ="transfer_type") + private String transactionType; + + @Column(name ="value") + private BigDecimal transactionValue; + + @Column(name ="status") + private String transactionStatus; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Column(name = "created_at") + private LocalDateTime createdAt; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @Column(name = "updated_at") + private LocalDateTime updatedAt; + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/request/RegisterTransactionRequest.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/request/RegisterTransactionRequest.java new file mode 100644 index 0000000..555b208 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/request/RegisterTransactionRequest.java @@ -0,0 +1,31 @@ +package com.yape.transactionmanagement.model.request; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class RegisterTransactionRequest { + + @NotBlank(message = "El campo accountExternalIdDebit no puede enviarse vacio") + private String accountExternalIdDebit; + + @NotBlank(message = "El campo accountExternalIdCredit no puede enviarse vacio") + private String accountExternalIdCredit; + + @NotNull (message = "El campo tranferTypeId es obligatorio") + private Integer tranferTypeId; + + @NotNull (message = "El campo tranferTypeId es obligatorio") + private BigDecimal value; + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/response/TransactionResponse.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/response/TransactionResponse.java new file mode 100644 index 0000000..f041478 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/model/response/TransactionResponse.java @@ -0,0 +1,25 @@ +package com.yape.transactionmanagement.model.response; + +import com.yape.transactionmanagement.model.common.TransactionState; +import com.yape.transactionmanagement.model.common.TransactionType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class TransactionResponse implements Serializable { + + private String transactionExternalId; + private TransactionType transactionType; + private TransactionState transactionStatus; + private BigDecimal value; + private String createdAt; + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/repository/TransactionRepository.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/repository/TransactionRepository.java new file mode 100644 index 0000000..90f446c --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/repository/TransactionRepository.java @@ -0,0 +1,29 @@ +package com.yape.transactionmanagement.repository; + +import com.yape.transactionmanagement.model.entity.TransactionEntity; +import jakarta.transaction.Transactional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface TransactionRepository extends JpaRepository { + + @Query(value = "SELECT * FROM public.find_all_transactions_sp()", nativeQuery = true) + List callGetTransactionsSP(); + + + @Query(value = "SELECT * FROM public.find_transaction_by_code_sp(:txCode)", nativeQuery = true) + TransactionEntity callGetTransactionByCode(@Param("txCode") String txCode); + + @Transactional + @Modifying + @Query(value = "UPDATE public.transaction SET status = :txState, updated_at = CURRENT_TIMESTAMP WHERE id = :txId", nativeQuery = true) + void updateTransactionState(@Param("txId") Long transactionId, + @Param("txState") String transactionState); + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/repository/connector/TransactionRepositoryConnector.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/repository/connector/TransactionRepositoryConnector.java new file mode 100644 index 0000000..4dca4c3 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/repository/connector/TransactionRepositoryConnector.java @@ -0,0 +1,16 @@ +package com.yape.transactionmanagement.repository.connector; + +import com.yape.transactionmanagement.model.entity.TransactionEntity; +import reactor.core.publisher.Mono; + +import java.util.List; + +public interface TransactionRepositoryConnector { + + Mono> findAllTransactions(); + + Mono findTransactionByCode(String transactionCode); + + Mono saveTransaction(TransactionEntity entity); + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/repository/connector/impl/TransactionRepositoryConnectorImpl.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/repository/connector/impl/TransactionRepositoryConnectorImpl.java new file mode 100644 index 0000000..bde4e40 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/repository/connector/impl/TransactionRepositoryConnectorImpl.java @@ -0,0 +1,45 @@ +package com.yape.transactionmanagement.repository.connector.impl; + +import com.yape.transactionmanagement.model.entity.TransactionEntity; +import com.yape.transactionmanagement.repository.TransactionRepository; +import com.yape.transactionmanagement.repository.connector.TransactionRepositoryConnector; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +@Slf4j +@Component +@RequiredArgsConstructor +public class TransactionRepositoryConnectorImpl implements TransactionRepositoryConnector { + + private final TransactionRepository transactionRepository; + + @Override + public Mono> findAllTransactions() { + return Mono.fromFuture(CompletableFuture.supplyAsync(transactionRepository::callGetTransactionsSP)) + .doOnSubscribe(sub -> log.info("Buscando transacciones")) + .doOnSuccess(success -> log.info("Busqueda exitosa")) + .doOnError(err -> log.info("Ocurrio un error en la busqueda de todas las transacciones")); + } + + @Override + public Mono findTransactionByCode(String transactionCode) { + return Mono.fromFuture(CompletableFuture.supplyAsync(() -> transactionRepository.callGetTransactionByCode(transactionCode))) + .doOnSubscribe(sub -> log.info("Buscando transaccion {}", transactionCode)) + .doOnSuccess(success -> log.info("Busqueda exitosa")) + .doOnError(err -> log.info("Ocurrio un error en la busqueda de la transaccion")); + } + + @Override + public Mono saveTransaction(TransactionEntity entity) { + return Mono.fromFuture(CompletableFuture.supplyAsync(() -> transactionRepository.save(entity))) + .doOnSubscribe(sub -> log.info("Registrando nueva transaccion")) + .doOnSuccess(success -> log.info("Transaccion se registro correctamente")) + .doOnError(err -> log.info("Ocurrio un error al intentar registrar la transaccion")); + } + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/service/TransactionManagementService.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/service/TransactionManagementService.java new file mode 100644 index 0000000..7476fed --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/service/TransactionManagementService.java @@ -0,0 +1,16 @@ +package com.yape.transactionmanagement.service; + +import com.yape.transactionmanagement.model.request.RegisterTransactionRequest; +import com.yape.transactionmanagement.model.response.TransactionResponse; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public interface TransactionManagementService { + + Flux getAllTransactions(); + + Mono getTransactionByCode(String transactionCode); + + Mono registerTransaction(RegisterTransactionRequest request); + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/service/impl/TransactionManagementServiceImpl.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/service/impl/TransactionManagementServiceImpl.java new file mode 100644 index 0000000..3382159 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/service/impl/TransactionManagementServiceImpl.java @@ -0,0 +1,59 @@ +package com.yape.transactionmanagement.service.impl; + +import com.yape.transactionmanagement.builder.TransactionBuilder; +import com.yape.transactionmanagement.kafka.producer.TransactionMessageProducer; +import com.yape.transactionmanagement.model.constant.TransactionStatusEnum; +import com.yape.transactionmanagement.model.request.RegisterTransactionRequest; +import com.yape.transactionmanagement.model.response.TransactionResponse; +import com.yape.transactionmanagement.repository.connector.TransactionRepositoryConnector; +import com.yape.transactionmanagement.service.TransactionManagementService; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Slf4j +@Service +@RequiredArgsConstructor +public class TransactionManagementServiceImpl implements TransactionManagementService { + + private final TransactionRepositoryConnector transactionRepositoryConnector; + private final TransactionMessageProducer transactionMessageProducer; + + @Cacheable(cacheNames = "transactions") + @Override + public Flux getAllTransactions() { + return transactionRepositoryConnector.findAllTransactions() + .flatMapMany(Flux::fromIterable) + .onBackpressureBuffer() + .map(TransactionBuilder::buildTransactionResponse); + } + + @Cacheable(cacheNames = "transaction", key = "#txCode", unless = "#result == null") + @Override + public Mono getTransactionByCode(String txCode) { + return transactionRepositoryConnector.findTransactionByCode(txCode) + .map(TransactionBuilder::buildTransactionResponse); + } + + @CacheEvict(cacheNames = "transactions", allEntries = true) + @Transactional + @Override + public Mono registerTransaction(RegisterTransactionRequest request) { + return Mono.fromCallable(() -> TransactionBuilder.buildTransactionEntity(request)) + .flatMap(entity -> transactionRepositoryConnector.saveTransaction(entity) + .flatMap(sEntity -> Mono.fromCallable(() -> transactionMessageProducer.sendMessage(TransactionBuilder.buildTransactionDTO(sEntity))) + .onErrorResume(err -> { + log.error("Updating state to REJECTED because can't evaluate transaction value. TxCode {}", sEntity.getTransactionCode()); + sEntity.setTransactionStatus(TransactionStatusEnum.REJECTED.getDescription()); + return transactionRepositoryConnector.saveTransaction(entity) + .thenReturn(Boolean.FALSE); + }))) + .then(); + } + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/util/Constants.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/util/Constants.java new file mode 100644 index 0000000..43c39c7 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/util/Constants.java @@ -0,0 +1,7 @@ +package com.yape.transactionmanagement.util; + +public class Constants { + + public static String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/util/Utils.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/util/Utils.java new file mode 100644 index 0000000..4b88c8c --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/util/Utils.java @@ -0,0 +1,12 @@ +package com.yape.transactionmanagement.util; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class Utils { + + public static String formatDateTime(LocalDateTime dateTime) { + return dateTime.format(DateTimeFormatter.ofPattern(Constants.DATE_TIME_FORMAT)); + } + +} diff --git a/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/web/TransactionManagementController.java b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/web/TransactionManagementController.java new file mode 100644 index 0000000..1a4d063 --- /dev/null +++ b/microservices/transaction-management/src/main/java/com/yape/transactionmanagement/web/TransactionManagementController.java @@ -0,0 +1,43 @@ +package com.yape.transactionmanagement.web; + +import com.yape.transactionmanagement.model.request.RegisterTransactionRequest; +import com.yape.transactionmanagement.model.response.TransactionResponse; +import com.yape.transactionmanagement.service.TransactionManagementService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Slf4j +@RestController +@RequestMapping("${application.core.api.path}/transactions") +@RequiredArgsConstructor +public class TransactionManagementController { + + private final TransactionManagementService transactionManagementService; + + @GetMapping(value = "", produces = MediaType.APPLICATION_NDJSON_VALUE) + public Flux getAllTransactions() { + return transactionManagementService.getAllTransactions() + .doOnSubscribe(sub -> log.info("Iniciando busqueda todas las transacciones")) + .doAfterTerminate(() -> log.info("Exito en la busqueda de todas las transacciones.")); + } + + @GetMapping(value = "/{transactionCode}") + public Mono getTransactionByCode(@PathVariable("transactionCode") String transactionCode) { + return transactionManagementService.getTransactionByCode(transactionCode) + .doOnSubscribe(sub -> log.info("Iniciando busqueda de transaccion por codigo {}", transactionCode)) + .doOnSuccess(response -> log.info("Exito en la busqueda de la transaccion.")); + } + + @PostMapping(value = "", consumes = MediaType.APPLICATION_JSON_VALUE) + public Mono registerTransaction(@Valid @RequestBody RegisterTransactionRequest request) { + return transactionManagementService.registerTransaction(request) + .doOnSubscribe(sub -> log.info("Iniciando registro de nueva transaccion")) + .doOnSuccess(response -> log.info("Exito en el registro de la transaccion.")); + } + +} diff --git a/microservices/transaction-management/src/main/resources/application.yml b/microservices/transaction-management/src/main/resources/application.yml new file mode 100644 index 0000000..9fe8130 --- /dev/null +++ b/microservices/transaction-management/src/main/resources/application.yml @@ -0,0 +1,46 @@ +application: + core: + api: + path: /api/v1.0/transaction-management + +server: + port: 8093 + +spring: + datasource: + url: jdbc:postgresql://localhost:5432/bd_challenge + username: postgres + password: postgres + driver-class-name: org.postgresql.Driver + jpa: + hibernate: + ddl-auto: none + kafka: + bootstrap-servers: 'localhost:9092' + consumer: + group-id: yape-group + offset-mode: 'earliest' + properties: + spring: + json: + trusted: + packages: com.yape.fraudevaluation.model.dto + producer: + properties: + spring: + json: + add: + type: + headers: false + key-serializer: org.springframework.kafka.support.serializer.JsonSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + +redis: + url: 'http://localhost:6379' + ttl-secconds: 60 + +transaction-management: + kafka-topics: + fraud-evaluation: 'topic-fraud' + transaction: 'topic-transaction' + diff --git a/script/01_init_query.sql b/script/01_init_query.sql new file mode 100644 index 0000000..06aa3e4 --- /dev/null +++ b/script/01_init_query.sql @@ -0,0 +1,30 @@ +CREATE TABLE public.transaction ( + "id" BIGSERIAL PRIMARY KEY, + "code" VARCHAR(100) NOT NULL, + "account_external_id_debit" VARCHAR(100) NOT NULL, + "account_external_id_credit" VARCHAR(100) NOT NULL, + "transfer_type" VARCHAR(50) NOT NULL, + "value" DECIMAL NOT NULL, + "status" VARCHAR(50) NOT NULL, + "created_at" TIMESTAMP, + "updated_at" TIMESTAMP, + CONSTRAINT "uq_code" UNIQUE ("code") +); + + + +CREATE OR REPLACE FUNCTION public.find_all_transactions_sp() RETURNS SETOF transaction AS $$ +BEGIN +RETURN QUERY SELECT * FROM public.transaction; +END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION public.find_transaction_by_code_sp(transaction_code VARCHAR) RETURNS transaction AS $$ +DECLARE +tx_result transaction%ROWTYPE; +BEGIN +SELECT * INTO tx_result FROM public.transaction WHERE "code" = transaction_code LIMIT 1; +RETURN tx_result; +END; +$$ LANGUAGE plpgsql; \ No newline at end of file