From 9b9baef95918dba8ba8248c3c1b6c80b2eff672e Mon Sep 17 00:00:00 2001 From: Alvaro Cubas Date: Mon, 24 Mar 2025 11:56:13 -0500 Subject: [PATCH] ACH: Implementacion del reto tecnico --- pom.xml | 103 ++++++++++++++++++ .../java/com/ibk/reto/RetoApplication.java | 20 ++++ .../ibk/reto/configuration/JacksonConfig.java | 22 ++++ .../controller/TransactionController.java | 31 ++++++ .../com/ibk/reto/kafka/KafkaConsumer.java | 45 ++++++++ .../com/ibk/reto/kafka/KafkaProducer.java | 58 ++++++++++ .../reto/kafka/TransactionKafkaConsumer.java | 46 ++++++++ .../java/com/ibk/reto/model/Transaction.java | 33 ++++++ .../ibk/reto/model/TransactionEstatus.java | 12 ++ .../repository/TransactionRepository.java | 16 +++ .../ibk/reto/service/TransactionService.java | 41 +++++++ src/main/resources/application.yml | 21 ++++ .../com/ibk/reto/RetoApplicationTests.java | 13 +++ 13 files changed, 461 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/com/ibk/reto/RetoApplication.java create mode 100644 src/main/java/com/ibk/reto/configuration/JacksonConfig.java create mode 100644 src/main/java/com/ibk/reto/controller/TransactionController.java create mode 100644 src/main/java/com/ibk/reto/kafka/KafkaConsumer.java create mode 100644 src/main/java/com/ibk/reto/kafka/KafkaProducer.java create mode 100644 src/main/java/com/ibk/reto/kafka/TransactionKafkaConsumer.java create mode 100644 src/main/java/com/ibk/reto/model/Transaction.java create mode 100644 src/main/java/com/ibk/reto/model/TransactionEstatus.java create mode 100644 src/main/java/com/ibk/reto/repository/TransactionRepository.java create mode 100644 src/main/java/com/ibk/reto/service/TransactionService.java create mode 100644 src/main/resources/application.yml create mode 100644 src/test/java/com/ibk/reto/RetoApplicationTests.java diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..1f1bc90 --- /dev/null +++ b/pom.xml @@ -0,0 +1,103 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.4 + + + com.ibk + reto + 0.0.1-SNAPSHOT + reto + Demo project for Spring Boot + + + + + + + + + + + + + + + 17 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.kafka + spring-kafka + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.15.0 + + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.kafka + spring-kafka-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/ibk/reto/RetoApplication.java b/src/main/java/com/ibk/reto/RetoApplication.java new file mode 100644 index 0000000..f1b9365 --- /dev/null +++ b/src/main/java/com/ibk/reto/RetoApplication.java @@ -0,0 +1,20 @@ +package com.ibk.reto; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Reto Tecnico IBK. + * + * @author Alvaro Cubas Huarca + * @version 1.0 + * @since 2025-03-23 + */ +@SpringBootApplication +public class RetoApplication { + + public static void main(String[] args) { + SpringApplication.run(RetoApplication.class, args); + } + +} diff --git a/src/main/java/com/ibk/reto/configuration/JacksonConfig.java b/src/main/java/com/ibk/reto/configuration/JacksonConfig.java new file mode 100644 index 0000000..f165428 --- /dev/null +++ b/src/main/java/com/ibk/reto/configuration/JacksonConfig.java @@ -0,0 +1,22 @@ +package com.ibk.reto.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Reto Tecnico IBK. + * + * @author Alvaro Cubas Huarca + * @version 1.0 + * @since 2025-03-23 + */ +@Configuration +public class JacksonConfig { + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper().registerModule(new JavaTimeModule()); + } + +} diff --git a/src/main/java/com/ibk/reto/controller/TransactionController.java b/src/main/java/com/ibk/reto/controller/TransactionController.java new file mode 100644 index 0000000..7408cef --- /dev/null +++ b/src/main/java/com/ibk/reto/controller/TransactionController.java @@ -0,0 +1,31 @@ +package com.ibk.reto.controller; + +import com.ibk.reto.model.Transaction; +import com.ibk.reto.service.TransactionService; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +/** + * Reto Tecnico IBK. + * + * @author Alvaro Cubas Huarca + * @version 1.0 + * @since 2025-03-23 + */ +@RestController +@RequestMapping("/transactions") +public class TransactionController { + private final TransactionService service; + public TransactionController(TransactionService service) { + this.service = service; + } + @PostMapping + public Transaction createTransaction(@RequestBody Transaction transaction) { + return service.createTransaction(transaction); + } + @GetMapping("/{id}") + public Transaction getTransaction(@PathVariable UUID id) { + return service.getTransactionById(id); + } +} diff --git a/src/main/java/com/ibk/reto/kafka/KafkaConsumer.java b/src/main/java/com/ibk/reto/kafka/KafkaConsumer.java new file mode 100644 index 0000000..c691b61 --- /dev/null +++ b/src/main/java/com/ibk/reto/kafka/KafkaConsumer.java @@ -0,0 +1,45 @@ +package com.ibk.reto.kafka; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.ibk.reto.model.Transaction; +import com.ibk.reto.model.TransactionEstatus; +import com.ibk.reto.service.TransactionService; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Service; + +/** + * Reto Tecnico IBK. + * + * @author Alvaro Cubas Huarca + * @version 1.0 + * @since 2025-03-23 + */ +@Service +public class KafkaConsumer { + private final KafkaProducer kafkaProducer; + private final ObjectMapper objectMapper; + + public KafkaConsumer(KafkaProducer kafkaProducer) { + this.kafkaProducer = kafkaProducer; + this.objectMapper = new ObjectMapper(); + this.objectMapper.registerModule(new JavaTimeModule()); + } + + @KafkaListener(topics = "transaction_created", groupId = "antifraud-group") + public void processTransaction(ConsumerRecord record) { + try { + Transaction transaction = objectMapper.readValue(record.value(), Transaction.class); + + // Lógica antifraude: rechazar transacciones > 1000 + TransactionEstatus status = transaction.getValue() > 1000 ? TransactionEstatus.RECHAZADO : TransactionEstatus.APROBADO; + + // Enviar evento de actualización de estado a transaction service + kafkaProducer.sendTransactionStatus(transaction.getTransactionExternalId(), status); + + } catch (Exception e) { + throw new RuntimeException("Error processing Kafka message in AntiFraud Service", e); + } + } +} diff --git a/src/main/java/com/ibk/reto/kafka/KafkaProducer.java b/src/main/java/com/ibk/reto/kafka/KafkaProducer.java new file mode 100644 index 0000000..e871b96 --- /dev/null +++ b/src/main/java/com/ibk/reto/kafka/KafkaProducer.java @@ -0,0 +1,58 @@ +package com.ibk.reto.kafka; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.ibk.reto.model.Transaction; +import com.ibk.reto.model.TransactionEstatus; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +/** + * Reto Tecnico IBK. + * + * @author Alvaro Cubas Huarca + * @version 1.0 + * @since 2025-03-23 + */ +@Service +public class KafkaProducer { + + private final KafkaTemplate kafkaTemplate; + private final ObjectMapper objectMapper; + + public KafkaProducer(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + this.objectMapper = new ObjectMapper(); + this.objectMapper.registerModule(new JavaTimeModule()); + } + + public void sendTransaction(Transaction transaction) { + try { + String message = objectMapper.writeValueAsString(transaction); + kafkaTemplate.send("transaction_created", message); + } catch (Exception e) { + throw new RuntimeException("Error sending Kafka message", e); + } + } + + public void sendTransactionStatus(UUID transactionId, TransactionEstatus status) { + try { + String message = objectMapper.writeValueAsString(new TransactionStatusUpdate(transactionId, status)); + kafkaTemplate.send("transaction_status_updated", message); + } catch (Exception e) { + throw new RuntimeException("Error sending Kafka message", e); + } + } + + private static class TransactionStatusUpdate { + public UUID transactionId; + public TransactionEstatus status; + + public TransactionStatusUpdate(UUID transactionId, TransactionEstatus status) { + this.transactionId = transactionId; + this.status = status; + } + } +} diff --git a/src/main/java/com/ibk/reto/kafka/TransactionKafkaConsumer.java b/src/main/java/com/ibk/reto/kafka/TransactionKafkaConsumer.java new file mode 100644 index 0000000..e0af108 --- /dev/null +++ b/src/main/java/com/ibk/reto/kafka/TransactionKafkaConsumer.java @@ -0,0 +1,46 @@ +package com.ibk.reto.kafka; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.ibk.reto.model.TransactionEstatus; +import com.ibk.reto.service.TransactionService; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +/** + * Reto Tecnico IBK. + * + * @author Alvaro Cubas Huarca + * @version 1.0 + * @since 2025-03-23 + */ +@Service +public class TransactionKafkaConsumer { + private final TransactionService transactionService; + private final ObjectMapper objectMapper; + + public TransactionKafkaConsumer(TransactionService transactionService) { + this.transactionService = transactionService; + this.objectMapper = new ObjectMapper(); + this.objectMapper.registerModule(new JavaTimeModule()); + } + + @KafkaListener(topics = "transaction_status_updated", groupId = "transaction-group") + public void updateTransactionStatus(ConsumerRecord record) { + try { + JsonNode jsonNode = objectMapper.readTree(record.value()); + UUID transactionId = UUID.fromString(jsonNode.get("transactionId").asText()); + TransactionEstatus status = TransactionEstatus.valueOf(jsonNode.get("status").asText()); + + // Actualizar estado en la base de datos + transactionService.updateTransactionStatus(transactionId, status); + + } catch (Exception e) { + throw new RuntimeException("Error processing Kafka message in Transaction Service", e); + } + } +} diff --git a/src/main/java/com/ibk/reto/model/Transaction.java b/src/main/java/com/ibk/reto/model/Transaction.java new file mode 100644 index 0000000..0473d98 --- /dev/null +++ b/src/main/java/com/ibk/reto/model/Transaction.java @@ -0,0 +1,33 @@ +package com.ibk.reto.model; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.*; +import lombok.Data; +import java.time.LocalDateTime; +import java.util.UUID; + +/** + * Reto Tecnico IBK. + * + * @author Alvaro Cubas Huarca + * @version 1.0 + * @since 2025-03-23 + */ +@Entity +@Data +public class Transaction { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private UUID transactionExternalId; + + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private Integer tranferTypeId; + private Double value; + + @Enumerated(EnumType.STRING) + private TransactionEstatus transactionStatus; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createdAt = LocalDateTime.now(); +} diff --git a/src/main/java/com/ibk/reto/model/TransactionEstatus.java b/src/main/java/com/ibk/reto/model/TransactionEstatus.java new file mode 100644 index 0000000..2b697b5 --- /dev/null +++ b/src/main/java/com/ibk/reto/model/TransactionEstatus.java @@ -0,0 +1,12 @@ +package com.ibk.reto.model; + +/** + * Reto Tecnico IBK. + * + * @author Alvaro Cubas Huarca + * @version 1.0 + * @since 2025-03-23 + */ +public enum TransactionEstatus { + PENDIENTE, APROBADO, RECHAZADO +} diff --git a/src/main/java/com/ibk/reto/repository/TransactionRepository.java b/src/main/java/com/ibk/reto/repository/TransactionRepository.java new file mode 100644 index 0000000..32b3bae --- /dev/null +++ b/src/main/java/com/ibk/reto/repository/TransactionRepository.java @@ -0,0 +1,16 @@ +package com.ibk.reto.repository; + +import com.ibk.reto.model.Transaction; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +/** + * Reto Tecnico IBK. + * + * @author Alvaro Cubas Huarca + * @version 1.0 + * @since 2025-03-23 + */ +public interface TransactionRepository extends JpaRepository { +} diff --git a/src/main/java/com/ibk/reto/service/TransactionService.java b/src/main/java/com/ibk/reto/service/TransactionService.java new file mode 100644 index 0000000..4167c2c --- /dev/null +++ b/src/main/java/com/ibk/reto/service/TransactionService.java @@ -0,0 +1,41 @@ +package com.ibk.reto.service; + +import com.ibk.reto.kafka.KafkaProducer; +import com.ibk.reto.model.Transaction; +import com.ibk.reto.model.TransactionEstatus; +import com.ibk.reto.repository.TransactionRepository; +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +public class TransactionService { + private final TransactionRepository repository; + private final KafkaProducer kafkaProducer; + + public TransactionService(TransactionRepository repository, KafkaProducer kafkaProducer) { + this.repository = repository; + this.kafkaProducer = kafkaProducer; + } + + @Transactional + public Transaction createTransaction(Transaction transaction) { + transaction.setTransactionStatus(TransactionEstatus.PENDIENTE); + Transaction savedTransaction = repository.save(transaction); + kafkaProducer.sendTransaction(savedTransaction); + return savedTransaction; + } + + public Transaction getTransactionById(UUID id) { + return repository.findById(id).orElseThrow(() -> new RuntimeException("Transaction not found")); + } + + @Transactional + public void updateTransactionStatus(UUID transactionId, TransactionEstatus status) { + Transaction transaction = getTransactionById(transactionId); + transaction.setTransactionStatus(status); + repository.save(transaction); + } + +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..425149d --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,21 @@ +spring: + datasource: + url: jdbc:postgresql://localhost:5432/postgres + username: postgres + password: 123456 + driver-class-name: org.postgresql.Driver + jpa: + hibernate: + ddl-auto: update + show-sql: true + jackson: + serialization: + WRITE_DATES_AS_TIMESTAMPS: false + kafka: + bootstrap-servers: localhost:9092 + consumer: + group-id: transaction-group + auto-offset-reset: earliest + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.apache.kafka.common.serialization.StringSerializer \ No newline at end of file diff --git a/src/test/java/com/ibk/reto/RetoApplicationTests.java b/src/test/java/com/ibk/reto/RetoApplicationTests.java new file mode 100644 index 0000000..3be625e --- /dev/null +++ b/src/test/java/com/ibk/reto/RetoApplicationTests.java @@ -0,0 +1,13 @@ +package com.ibk.reto; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class RetoApplicationTests { + + /*@Test + void contextLoads() { + }*/ + +}