diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..a0ccf77bc5 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,5 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Environment-dependent path to Maven home directory +/mavenHomeManager.xml diff --git a/.idea/app-nodejs-codechallenge.iml b/.idea/app-nodejs-codechallenge.iml new file mode 100644 index 0000000000..d6ebd48059 --- /dev/null +++ b/.idea/app-nodejs-codechallenge.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000000..afd4c0645f --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000000..ec0bcf22a0 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..b75ee77dfd --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000..65867f62de --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..35eb1ddfbb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/anti-fraud-service/pom.xml b/anti-fraud-service/pom.xml new file mode 100644 index 0000000000..c540ad8f7e --- /dev/null +++ b/anti-fraud-service/pom.xml @@ -0,0 +1,83 @@ + + 4.0.0 + com.yape + anti-fraud-service + 1.0.0 + + + 21 + 3.2.1 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.kafka + spring-kafka + + + com.h2database + h2 + runtime + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + org.projectlombok + lombok + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + 21 + + + + + diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/AntiFraudApplication.java b/anti-fraud-service/src/main/java/com/yape/antifraud/AntiFraudApplication.java new file mode 100644 index 0000000000..b79fb35c2f --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/AntiFraudApplication.java @@ -0,0 +1,11 @@ +package com.yape.antifraud; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AntiFraudApplication { + public static void main(String[] args) { + SpringApplication.run(AntiFraudApplication.class, args); + } +} diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/FraudDecisionService.java b/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/FraudDecisionService.java new file mode 100644 index 0000000000..9873cd95ce --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/FraudDecisionService.java @@ -0,0 +1,8 @@ +package com.yape.antifraud.aplication; + +import com.yape.antifraud.kafka.models.TransactionReqEvent; + +public interface FraudDecisionService { + + void analyze(TransactionReqEvent transaction); +} diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/ProducerService.java b/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/ProducerService.java new file mode 100644 index 0000000000..92d2a65c2d --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/ProducerService.java @@ -0,0 +1,8 @@ +package com.yape.antifraud.aplication; + +import com.yape.antifraud.kafka.models.TransactionRespEvent; + +public interface ProducerService { + + void sendMessage(TransactionRespEvent transaction); +} diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/impl/FraudDetectionService.java b/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/impl/FraudDetectionService.java new file mode 100644 index 0000000000..df00fbe395 --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/impl/FraudDetectionService.java @@ -0,0 +1,35 @@ +package com.yape.antifraud.aplication.impl; + +import com.yape.antifraud.kafka.models.TransactionRespEvent; +import com.yape.antifraud.model.TransactionStatus; +import com.yape.antifraud.kafka.models.TransactionReqEvent; +import com.yape.antifraud.aplication.FraudDecisionService; +import com.yape.antifraud.aplication.ProducerService; +import org.springframework.stereotype.Service; + +@Service +public class FraudDetectionService implements FraudDecisionService { + + private final FraudPolicy fraudPolicy; + private final ProducerService producer; + + public FraudDetectionService(FraudPolicy fraudPolicy, ProducerService producer){ + this.fraudPolicy = fraudPolicy; + this.producer = producer; + } + + @Override + public void analyze(TransactionReqEvent transaction) { + + var isFraud = fraudPolicy.isFraud(transaction.getAmount()); + var statusTrx = isFraud? TransactionStatus.REJECTED : TransactionStatus.APPROVED; + transaction.setStatus(statusTrx); + + var updateTransaction = getUpdateEvent(transaction); + producer.sendMessage(updateTransaction); + } + + private TransactionRespEvent getUpdateEvent(TransactionReqEvent transaction){ + return new TransactionRespEvent(transaction.getId(), transaction.getStatus()); + } +} diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/impl/FraudPolicy.java b/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/impl/FraudPolicy.java new file mode 100644 index 0000000000..19831862f3 --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/aplication/impl/FraudPolicy.java @@ -0,0 +1,12 @@ +package com.yape.antifraud.aplication.impl; + +import org.springframework.stereotype.Component; +import static com.yape.antifraud.utils.Constants.MAX_ALLOWED_VALUE; + +@Component +public class FraudPolicy { + + public boolean isFraud(double amount) { + return amount > MAX_ALLOWED_VALUE; + } +} diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/config/KafkaConfig.java b/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/config/KafkaConfig.java new file mode 100644 index 0000000000..1b0e188861 --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/config/KafkaConfig.java @@ -0,0 +1,65 @@ +package com.yape.antifraud.kafka.config; + +import java.util.HashMap; +import java.util.Map; +import com.yape.antifraud.kafka.models.TransactionReqEvent; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.*; +import org.springframework.kafka.support.serializer.JsonDeserializer; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import static com.yape.antifraud.utils.Constants.TOPIC_SERVICE_GROUP; + + +@EnableKafka +@Configuration +public class KafkaConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Bean + public ProducerFactory producerFactory() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + props.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false); + return new DefaultKafkaProducerFactory<>(props); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } + + @Bean + public ConsumerFactory consumerFactory() { + + JsonDeserializer deserializer = new JsonDeserializer<>(TransactionReqEvent.class); + deserializer.addTrustedPackages("*"); + deserializer.setRemoveTypeHeaders(true); + + Map props = new HashMap<>(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ConsumerConfig.GROUP_ID_CONFIG, TOPIC_SERVICE_GROUP);//"anti-fraud-group" + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); + return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), deserializer); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + return factory; + } +} diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/event/TransactionConsumer.java b/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/event/TransactionConsumer.java new file mode 100644 index 0000000000..f0894b196e --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/event/TransactionConsumer.java @@ -0,0 +1,31 @@ +package com.yape.antifraud.kafka.event; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.yape.antifraud.model.TransactionStatus; +import com.yape.antifraud.aplication.impl.FraudDetectionService; +import com.yape.antifraud.aplication.FraudDecisionService; +import com.yape.antifraud.kafka.models.TransactionReqEvent; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import static com.yape.antifraud.utils.Constants.TOPIC_CREATED; +import static com.yape.antifraud.utils.Constants.TOPIC_SERVICE_GROUP; + +@Component +public class TransactionConsumer { + + private static final Logger log = LoggerFactory.getLogger(TransactionConsumer.class); + private final FraudDecisionService fraudService; + + public TransactionConsumer(FraudDetectionService fraudService) { + this.fraudService = fraudService; + } + + @KafkaListener(topics = TOPIC_CREATED, groupId = TOPIC_SERVICE_GROUP) + public void consume(TransactionReqEvent transaction) { + + fraudService.analyze(transaction); + log.info("📦 [ANTI-FRAUD-SERVICE LISTENER] {} -> {} {}", transaction.getId(), transaction.getStatus(), transaction.getStatus() == TransactionStatus.APPROVED ? "✅" : transaction.getStatus() == TransactionStatus.REJECTED ? "❌" : "⏳"); + } +} diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/event/TransactionProducer.java b/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/event/TransactionProducer.java new file mode 100644 index 0000000000..7c4d72a7c7 --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/event/TransactionProducer.java @@ -0,0 +1,29 @@ +package com.yape.antifraud.kafka.event; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.yape.antifraud.aplication.ProducerService; +import com.yape.antifraud.kafka.models.TransactionRespEvent; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +import static com.yape.antifraud.utils.Constants.TOPIC_UPDATE; + + +@Component +public class TransactionProducer implements ProducerService { + + private static final Logger log = LoggerFactory.getLogger(TransactionProducer.class); + private final KafkaTemplate kafkaTemplate; + + public TransactionProducer(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Override + public void sendMessage(TransactionRespEvent transaction) { + + kafkaTemplate.send(TOPIC_UPDATE, transaction); + log.info("📦 [ANTI-FRAUD-SERVICE PRODUCER] {} -> {}", transaction.id(), transaction.status()); + } +} diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/models/TransactionReqEvent.java b/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/models/TransactionReqEvent.java new file mode 100644 index 0000000000..c92b4d6e2b --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/models/TransactionReqEvent.java @@ -0,0 +1,42 @@ +package com.yape.antifraud.kafka.models; + +import java.util.UUID; +import com.yape.antifraud.model.TransactionStatus; + +public class TransactionReqEvent { + private UUID id; + private double amount; + private TransactionStatus status; + + public TransactionReqEvent() {} + + public TransactionReqEvent(UUID id, double amount, TransactionStatus status) { + this.id = id; + this.amount = amount; + this.status = status; + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public double getAmount() { + return amount; + } + + public void setAmount(double amount) { + this.amount = amount; + } + + public TransactionStatus getStatus() { + return status; + } + + public void setStatus(TransactionStatus status) { + this.status = status; + } +} diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/models/TransactionRespEvent.java b/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/models/TransactionRespEvent.java new file mode 100644 index 0000000000..fe9a78fbce --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/kafka/models/TransactionRespEvent.java @@ -0,0 +1,10 @@ +package com.yape.antifraud.kafka.models; + +import com.yape.antifraud.model.TransactionStatus; + +import java.util.UUID; + +public record TransactionRespEvent( + UUID id, + TransactionStatus status +) {} diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/model/TransactionStatus.java b/anti-fraud-service/src/main/java/com/yape/antifraud/model/TransactionStatus.java new file mode 100644 index 0000000000..38617024f3 --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/model/TransactionStatus.java @@ -0,0 +1,7 @@ +package com.yape.antifraud.model; + +public enum TransactionStatus { + APPROVED, + PENDING, + REJECTED +} diff --git a/anti-fraud-service/src/main/java/com/yape/antifraud/utils/Constants.java b/anti-fraud-service/src/main/java/com/yape/antifraud/utils/Constants.java new file mode 100644 index 0000000000..8e2c6c7689 --- /dev/null +++ b/anti-fraud-service/src/main/java/com/yape/antifraud/utils/Constants.java @@ -0,0 +1,12 @@ +package com.yape.antifraud.utils; + +public class Constants { + private Constants() {} + + public static final double MAX_ALLOWED_VALUE = 1000; + + public static final String TOPIC_UPDATE = "transaction-updated"; + public static final String TOPIC_CREATED = "transaction-created"; + + public static final String TOPIC_SERVICE_GROUP = "transaction-service-group"; +} diff --git a/anti-fraud-service/src/main/resources/application.yaml b/anti-fraud-service/src/main/resources/application.yaml new file mode 100644 index 0000000000..18c9ae968f --- /dev/null +++ b/anti-fraud-service/src/main/resources/application.yaml @@ -0,0 +1,18 @@ +spring: + application: + name: anti-fraud-service + kafka: + bootstrap-servers: ${KAFKA_SERVER:localhost:9092} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + acks: all + consumer: + group-id: transaction-service-group + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + +server: + port: 8085 \ No newline at end of file diff --git a/anti-fraud-service/src/test/java/com/yape/antifraud/FraudDetectionServiceTest.java b/anti-fraud-service/src/test/java/com/yape/antifraud/FraudDetectionServiceTest.java new file mode 100644 index 0000000000..c9063afcfe --- /dev/null +++ b/anti-fraud-service/src/test/java/com/yape/antifraud/FraudDetectionServiceTest.java @@ -0,0 +1,63 @@ +package com.yape.antifraud; + +import java.util.UUID; +import com.yape.antifraud.aplication.ProducerService; +import com.yape.antifraud.aplication.impl.FraudDetectionService; +import com.yape.antifraud.aplication.impl.FraudPolicy; +import com.yape.antifraud.kafka.models.TransactionReqEvent; +import com.yape.antifraud.kafka.models.TransactionRespEvent; +import com.yape.antifraud.model.TransactionStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static com.yape.antifraud.utils.Constants.MAX_ALLOWED_VALUE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class FraudDetectionServiceTest { + + @Mock + private ProducerService producerService; + + private FraudDetectionService service; + + @BeforeEach + void setup() { + FraudPolicy fraudPolicy = new FraudPolicy(); // REAL + service = new FraudDetectionService(fraudPolicy, producerService); + } + + @Test + void shouldTransactionNotFraud() { + TransactionReqEvent request = new TransactionReqEvent(UUID.randomUUID(), MAX_ALLOWED_VALUE - 10, null); + + service.analyze(request); + + ArgumentCaptor captor = ArgumentCaptor.forClass(TransactionRespEvent.class); + + verify(producerService).sendMessage(captor.capture()); + + assertThat(request.getStatus()).isEqualTo(TransactionStatus.APPROVED); + assertThat(captor.getValue().status()).isEqualTo(TransactionStatus.APPROVED); + } + + @Test + void shouldTransactionIsFraud() { + TransactionReqEvent request = new TransactionReqEvent(UUID.randomUUID(), MAX_ALLOWED_VALUE + 1, null); + + service.analyze(request); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(TransactionRespEvent.class); + + verify(producerService).sendMessage(captor.capture()); + + assertThat(request.getStatus()).isEqualTo(TransactionStatus.REJECTED); + assertThat(captor.getValue().status()).isEqualTo(TransactionStatus.REJECTED); + } +} diff --git a/anti-fraud-service/target/classes/application.yaml b/anti-fraud-service/target/classes/application.yaml new file mode 100644 index 0000000000..18c9ae968f --- /dev/null +++ b/anti-fraud-service/target/classes/application.yaml @@ -0,0 +1,18 @@ +spring: + application: + name: anti-fraud-service + kafka: + bootstrap-servers: ${KAFKA_SERVER:localhost:9092} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + acks: all + consumer: + group-id: transaction-service-group + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + +server: + port: 8085 \ No newline at end of file diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/AntiFraudApplication.class b/anti-fraud-service/target/classes/com/yape/antifraud/AntiFraudApplication.class new file mode 100644 index 0000000000..b62861fbbe Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/AntiFraudApplication.class differ diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/aplication/FraudDecisionService.class b/anti-fraud-service/target/classes/com/yape/antifraud/aplication/FraudDecisionService.class new file mode 100644 index 0000000000..71cede18cf Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/aplication/FraudDecisionService.class differ diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/aplication/ProducerService.class b/anti-fraud-service/target/classes/com/yape/antifraud/aplication/ProducerService.class new file mode 100644 index 0000000000..e68d60bf32 Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/aplication/ProducerService.class differ diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/aplication/impl/FraudDetectionService.class b/anti-fraud-service/target/classes/com/yape/antifraud/aplication/impl/FraudDetectionService.class new file mode 100644 index 0000000000..3e9ff8b2e7 Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/aplication/impl/FraudDetectionService.class differ diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/aplication/impl/FraudPolicy.class b/anti-fraud-service/target/classes/com/yape/antifraud/aplication/impl/FraudPolicy.class new file mode 100644 index 0000000000..c00e0ff5ed Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/aplication/impl/FraudPolicy.class differ diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/kafka/config/KafkaConfig.class b/anti-fraud-service/target/classes/com/yape/antifraud/kafka/config/KafkaConfig.class new file mode 100644 index 0000000000..fbfc6cd769 Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/kafka/config/KafkaConfig.class differ diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/kafka/event/TransactionConsumer.class b/anti-fraud-service/target/classes/com/yape/antifraud/kafka/event/TransactionConsumer.class new file mode 100644 index 0000000000..eff730575f Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/kafka/event/TransactionConsumer.class differ diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/kafka/event/TransactionProducer.class b/anti-fraud-service/target/classes/com/yape/antifraud/kafka/event/TransactionProducer.class new file mode 100644 index 0000000000..81e45698e9 Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/kafka/event/TransactionProducer.class differ diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/kafka/models/TransactionReqEvent.class b/anti-fraud-service/target/classes/com/yape/antifraud/kafka/models/TransactionReqEvent.class new file mode 100644 index 0000000000..708af422bd Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/kafka/models/TransactionReqEvent.class differ diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/kafka/models/TransactionRespEvent.class b/anti-fraud-service/target/classes/com/yape/antifraud/kafka/models/TransactionRespEvent.class new file mode 100644 index 0000000000..a31a2f12ba Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/kafka/models/TransactionRespEvent.class differ diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/model/TransactionStatus.class b/anti-fraud-service/target/classes/com/yape/antifraud/model/TransactionStatus.class new file mode 100644 index 0000000000..27a44d31e1 Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/model/TransactionStatus.class differ diff --git a/anti-fraud-service/target/classes/com/yape/antifraud/utils/Constants.class b/anti-fraud-service/target/classes/com/yape/antifraud/utils/Constants.class new file mode 100644 index 0000000000..d4a515921c Binary files /dev/null and b/anti-fraud-service/target/classes/com/yape/antifraud/utils/Constants.class differ diff --git a/anti-fraud-service/target/test-classes/com/yape/antifraud/FraudDetectionServiceTest.class b/anti-fraud-service/target/test-classes/com/yape/antifraud/FraudDetectionServiceTest.class new file mode 100644 index 0000000000..5ab2a71a48 Binary files /dev/null and b/anti-fraud-service/target/test-classes/com/yape/antifraud/FraudDetectionServiceTest.class differ diff --git a/transaction-service/pom.xml b/transaction-service/pom.xml new file mode 100644 index 0000000000..413fc8f2a4 --- /dev/null +++ b/transaction-service/pom.xml @@ -0,0 +1,88 @@ + + 4.0.0 + com.yape + transaction-service + 1.0.0 + + + 21 + 3.2.1 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.kafka + spring-kafka + + + com.h2database + h2 + runtime + + + org.projectlombok + lombok + 1.18.36 + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + 21 + 21 + + + + + diff --git a/transaction-service/src/main/java/com/yape/transaction/TransactionApplication.java b/transaction-service/src/main/java/com/yape/transaction/TransactionApplication.java new file mode 100644 index 0000000000..2fda944151 --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/TransactionApplication.java @@ -0,0 +1,12 @@ + +package com.yape.transaction; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TransactionApplication { + public static void main(String[] args) { + SpringApplication.run(TransactionApplication.class, args); + } +} diff --git a/transaction-service/src/main/java/com/yape/transaction/application/port/in/TransactionUseCase.java b/transaction-service/src/main/java/com/yape/transaction/application/port/in/TransactionUseCase.java new file mode 100644 index 0000000000..988beefc89 --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/application/port/in/TransactionUseCase.java @@ -0,0 +1,12 @@ +package com.yape.transaction.application.port.in; + +import java.util.List; +import java.util.UUID; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.infrastructure.rest.dto.TransactionRequest; + +public interface TransactionUseCase { + Transaction create(TransactionRequest request); + Transaction getById(UUID transactionId); + List getAllTransactions(); +} diff --git a/transaction-service/src/main/java/com/yape/transaction/application/port/in/UpdateTransactionUseCase.java b/transaction-service/src/main/java/com/yape/transaction/application/port/in/UpdateTransactionUseCase.java new file mode 100644 index 0000000000..f9f5eed660 --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/application/port/in/UpdateTransactionUseCase.java @@ -0,0 +1,8 @@ +package com.yape.transaction.application.port.in; + +import java.util.UUID; +import com.yape.transaction.domain.model.TransactionStatus; + +public interface UpdateTransactionUseCase { + void updateStatus(UUID transactionId, TransactionStatus status); +} \ No newline at end of file diff --git a/transaction-service/src/main/java/com/yape/transaction/application/port/out/TransactionEventPublisher.java b/transaction-service/src/main/java/com/yape/transaction/application/port/out/TransactionEventPublisher.java new file mode 100644 index 0000000000..574ecbb1d8 --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/application/port/out/TransactionEventPublisher.java @@ -0,0 +1,7 @@ +package com.yape.transaction.application.port.out; + +import com.yape.transaction.domain.model.Transaction; + +public interface TransactionEventPublisher { + void publish(Transaction transaction); +} diff --git a/transaction-service/src/main/java/com/yape/transaction/application/service/TransactionService.java b/transaction-service/src/main/java/com/yape/transaction/application/service/TransactionService.java new file mode 100644 index 0000000000..0c779fc3f0 --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/application/service/TransactionService.java @@ -0,0 +1,46 @@ +package com.yape.transaction.application.service; + +import java.util.List; +import java.util.UUID; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.application.port.in.TransactionUseCase; +import com.yape.transaction.application.port.out.TransactionEventPublisher; +import com.yape.transaction.domain.port.out.TransactionRepository; +import com.yape.transaction.infrastructure.rest.dto.TransactionRequest; +import org.springframework.stereotype.Service; + +@Service +public class TransactionService implements TransactionUseCase { + + private final TransactionRepository repositoryAdapter; + private final TransactionEventPublisher publisher; + + public TransactionService(TransactionRepository repository, TransactionEventPublisher publisher) { + this.repositoryAdapter = repository; + this.publisher = publisher; + } + + @Override + public Transaction create(TransactionRequest request) { + Transaction tx = new Transaction(UUID.randomUUID(), request.getAmount()); + + repositoryAdapter.save(tx); + publisher.publish(tx); + return tx; + } + + @Override + public Transaction getById(UUID transactionId) { + return repositoryAdapter.findById(transactionId) + .orElseThrow(() -> new IllegalStateException("Transaction not found")); + } + + @Override + public List getAllTransactions() { + + var listTransaction = repositoryAdapter.findAll(); + if(listTransaction.isEmpty()) + throw new IllegalStateException("Transactions not found"); + return listTransaction; + } +} diff --git a/transaction-service/src/main/java/com/yape/transaction/application/service/UpdateTransactionService.java b/transaction-service/src/main/java/com/yape/transaction/application/service/UpdateTransactionService.java new file mode 100644 index 0000000000..b4ba0ac679 --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/application/service/UpdateTransactionService.java @@ -0,0 +1,26 @@ +package com.yape.transaction.application.service; + +import java.util.UUID; +import org.springframework.stereotype.Service; +import com.yape.transaction.application.port.in.UpdateTransactionUseCase; +import com.yape.transaction.domain.model.TransactionStatus; +import com.yape.transaction.domain.port.out.TransactionRepository; + +@Service +public class UpdateTransactionService implements UpdateTransactionUseCase { + + private final TransactionRepository repositoryApater; + public UpdateTransactionService(TransactionRepository repository) { + this.repositoryApater = repository; + } + + @Override + public void updateStatus(UUID transactionId, TransactionStatus status) { + + var transaction = repositoryApater.findById(transactionId) + .orElseThrow(() -> new IllegalStateException("Transaction not found")); + + transaction.setStatus(status); + repositoryApater.save(transaction); + } +} diff --git a/transaction-service/src/main/java/com/yape/transaction/application/utils/Constants.java b/transaction-service/src/main/java/com/yape/transaction/application/utils/Constants.java new file mode 100644 index 0000000000..cff9b0ea1d --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/application/utils/Constants.java @@ -0,0 +1,10 @@ +package com.yape.transaction.application.utils; + +public class Constants { + private Constants() {} + + public static final String TOPIC_UPDATE = "transaction-updated"; + public static final String TOPIC_CREATED = "transaction-created"; + + public static final String TOPIC_SERVICE_GROUP = "transaction-service-group"; +} diff --git a/transaction-service/src/main/java/com/yape/transaction/domain/model/Transaction.java b/transaction-service/src/main/java/com/yape/transaction/domain/model/Transaction.java new file mode 100644 index 0000000000..1c5a278bcd --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/domain/model/Transaction.java @@ -0,0 +1,32 @@ +package com.yape.transaction.domain.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import java.time.Instant; +import java.util.UUID; + +@Setter +@Getter +@RequiredArgsConstructor +public class Transaction { + + private final UUID id; + private final double amount; + private TransactionStatus status; + private final Instant createdAt; + + public Transaction(UUID id, double amount) { + this.id = id; + this.amount = amount; + this.status = TransactionStatus.PENDING; + this.createdAt = Instant.now(); + } + + public Transaction(UUID id, double amount, TransactionStatus status, Instant createdAt) { + this.id = id; + this.amount = amount; + this.status = status; + this.createdAt = createdAt; + } +} diff --git a/transaction-service/src/main/java/com/yape/transaction/domain/model/TransactionStatus.java b/transaction-service/src/main/java/com/yape/transaction/domain/model/TransactionStatus.java new file mode 100644 index 0000000000..6b66bea37c --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/domain/model/TransactionStatus.java @@ -0,0 +1,6 @@ + +package com.yape.transaction.domain.model; + +public enum TransactionStatus { + PENDING, APPROVED, REJECTED +} diff --git a/transaction-service/src/main/java/com/yape/transaction/domain/port/out/TransactionRepository.java b/transaction-service/src/main/java/com/yape/transaction/domain/port/out/TransactionRepository.java new file mode 100644 index 0000000000..ca39f900e3 --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/domain/port/out/TransactionRepository.java @@ -0,0 +1,12 @@ +package com.yape.transaction.domain.port.out; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import com.yape.transaction.domain.model.Transaction; + +public interface TransactionRepository { + Transaction save(Transaction transaction); + Optional findById(UUID id); + List findAll(); +} diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/config/KafkaConfig.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/config/KafkaConfig.java new file mode 100644 index 0000000000..9bcc66a30e --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/config/KafkaConfig.java @@ -0,0 +1,64 @@ +package com.yape.transaction.infrastructure.config; + +import java.util.HashMap; +import java.util.Map; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.infrastructure.kafka.events.TransactionUpdatedEvent; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.core.*; +import org.springframework.kafka.support.serializer.JsonDeserializer; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import static com.yape.transaction.application.utils.Constants.TOPIC_SERVICE_GROUP; + +@EnableKafka +@Configuration +public class KafkaConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Bean + public ProducerFactory producerFactory() { + Map props = new HashMap<>(); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + props.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false); + return new DefaultKafkaProducerFactory<>(props); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } + + @Bean + public ConsumerFactory consumerFactory() { + JsonDeserializer deserializer = new JsonDeserializer<>(TransactionUpdatedEvent.class); + deserializer.addTrustedPackages("*"); + deserializer.setRemoveTypeHeaders(true); + + Map props = new HashMap<>(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ConsumerConfig.GROUP_ID_CONFIG, TOPIC_SERVICE_GROUP); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest"); + return new DefaultKafkaConsumerFactory<>(props, new StringDeserializer(), deserializer); + } + + @Bean + public ConcurrentKafkaListenerContainerFactory kafkaListenerContainerFactory() { + + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + return factory; + } +} diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/db/JpaRepository.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/db/JpaRepository.java new file mode 100644 index 0000000000..2e49722350 --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/db/JpaRepository.java @@ -0,0 +1,8 @@ + +package com.yape.transaction.infrastructure.db; + +import java.util.UUID; +import com.yape.transaction.infrastructure.db.entity.TransactionEntity; + +public interface JpaRepository extends org.springframework.data.jpa.repository.JpaRepository { +} diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/db/TransactionRepositoryAdapter.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/db/TransactionRepositoryAdapter.java new file mode 100644 index 0000000000..1e11a9839d --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/db/TransactionRepositoryAdapter.java @@ -0,0 +1,39 @@ +package com.yape.transaction.infrastructure.db; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.domain.port.out.TransactionRepository; +import com.yape.transaction.infrastructure.db.entity.TransactionEntity; +import org.springframework.stereotype.Component; + +@Component +public class TransactionRepositoryAdapter implements TransactionRepository { + + private final JpaRepository jpaRepository; + + public TransactionRepositoryAdapter(JpaRepository jpaRepository) { + this.jpaRepository = jpaRepository; + } + + @Override + public Transaction save(Transaction tx) { + jpaRepository.save(TransactionEntity.from(tx)); + return tx; + } + + @Override + public Optional findById(UUID id) { + return jpaRepository.findById(id) + .map(TransactionEntity::toDomain); + } + + @Override + public List findAll() { + return jpaRepository.findAll() + .stream() + .map(TransactionEntity::toDomain) + .toList(); + } +} diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/db/entity/TransactionEntity.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/db/entity/TransactionEntity.java new file mode 100644 index 0000000000..e9977a56fb --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/db/entity/TransactionEntity.java @@ -0,0 +1,59 @@ +package com.yape.transaction.infrastructure.db.entity; + +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.domain.model.TransactionStatus; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Entity; +import jakarta.persistence.Column; +import lombok.Getter; + +import java.time.Instant; +import java.util.UUID; + +@Getter +@Entity +@Table(name = "transactions") +public class TransactionEntity { + + @Id + @Column(name = "id", nullable = false, updatable = false) + private UUID id; + + @Column(name = "amount", nullable = false) + private double value; + + @Column(nullable = false) + private String status; + + @Column(name = "created_at", nullable = false) + private Instant createdAt; + + protected TransactionEntity() { + } + + private TransactionEntity( + UUID id, + double value, + String status, + Instant createdAt) { + + this.id = id; + this.value = value; + this.status = status; + this.createdAt = createdAt; + } + + public static TransactionEntity from(Transaction tx) { + return new TransactionEntity(tx.getId(), tx.getAmount(), tx.getStatus().name(), tx.getCreatedAt()); + } + + public Transaction toDomain() { + return new Transaction( + this.id, + this.value, + TransactionStatus.valueOf(this.status), + this.createdAt + ); + } +} diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/kafka/KafkaConsumer.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/kafka/KafkaConsumer.java new file mode 100644 index 0000000000..88016b5f5c --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/kafka/KafkaConsumer.java @@ -0,0 +1,30 @@ +package com.yape.transaction.infrastructure.kafka; + +import com.yape.transaction.domain.model.TransactionStatus; +import com.yape.transaction.infrastructure.kafka.events.TransactionUpdatedEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.kafka.annotation.KafkaListener; +import com.yape.transaction.application.port.in.UpdateTransactionUseCase; + +import static com.yape.transaction.application.utils.Constants.TOPIC_UPDATE; +import static com.yape.transaction.application.utils.Constants.TOPIC_SERVICE_GROUP; + +@Component +public class KafkaConsumer { + + private static final Logger log = LoggerFactory.getLogger(KafkaConsumer.class); + private final UpdateTransactionUseCase useCase; + + public KafkaConsumer(UpdateTransactionUseCase useCase) { + this.useCase = useCase; + } + + @KafkaListener(topics = TOPIC_UPDATE, groupId = TOPIC_SERVICE_GROUP) + public void consume(TransactionUpdatedEvent event) { + + useCase.updateStatus(event.id(), event.status()); + log.info("📦 [TRANSACTION-SERVICE LISTENER] {} -> {} {}", event.id(), event.status(), event.status() == TransactionStatus.APPROVED ? "✅" : event.status() == TransactionStatus.REJECTED ? "❌" : "⏳"); + } +} diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/kafka/KafkaProducer.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/kafka/KafkaProducer.java new file mode 100644 index 0000000000..6fff0a1aec --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/kafka/KafkaProducer.java @@ -0,0 +1,28 @@ +package com.yape.transaction.infrastructure.kafka; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.yape.transaction.application.port.out.TransactionEventPublisher; +import com.yape.transaction.domain.model.Transaction; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +import static com.yape.transaction.application.utils.Constants.TOPIC_CREATED; + +@Service +public class KafkaProducer implements TransactionEventPublisher { + + private static final Logger log = LoggerFactory.getLogger(KafkaProducer.class); + private final KafkaTemplate kafkaTemplate; + + public KafkaProducer(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Override + public void publish(Transaction transaction) { + + kafkaTemplate.send(TOPIC_CREATED, transaction); + log.info("📦 [TRANSACTION-SERVICE PRODUCER] {} -> {}", transaction.getId(), transaction.getStatus()); + } +} \ No newline at end of file diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/kafka/events/TransactionUpdatedEvent.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/kafka/events/TransactionUpdatedEvent.java new file mode 100644 index 0000000000..cd701f1e8c --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/kafka/events/TransactionUpdatedEvent.java @@ -0,0 +1,9 @@ +package com.yape.transaction.infrastructure.kafka.events; + +import java.util.UUID; +import com.yape.transaction.domain.model.TransactionStatus; + +public record TransactionUpdatedEvent( + UUID id, + TransactionStatus status +) {} diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/rest/TransactionController.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/rest/TransactionController.java new file mode 100644 index 0000000000..6f151235e4 --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/rest/TransactionController.java @@ -0,0 +1,49 @@ +package com.yape.transaction.infrastructure.rest; + +import java.util.List; +import java.util.UUID; + +import jakarta.validation.Valid; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import com.yape.transaction.application.port.in.TransactionUseCase; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.infrastructure.rest.dto.TransactionRequest; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@RestController +@RequestMapping("/transactions") +public class TransactionController { + + private static final Logger log = LoggerFactory.getLogger(TransactionController.class); + + private final TransactionUseCase useCase; + + public TransactionController(TransactionUseCase useCase) { + this.useCase = useCase; + } + + @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) + public Transaction create(@RequestBody @Valid TransactionRequest request) { + log.info("Creando transacción para usuario: {}", request.getAmount()); + return useCase.create(request); + } + + @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + public List getAll() { + log.info("Obteniendo todas las transacciones"); + return useCase.getAllTransactions(); + } + + @GetMapping(value = "/{id}", produces = MediaType.APPLICATION_JSON_VALUE) + public Transaction getById(@PathVariable("id") UUID transactionId) { + log.info("Buscando transacción con ID: {}", transactionId); + return useCase.getById(transactionId); + } +} diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/rest/dto/TransactionRequest.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/rest/dto/TransactionRequest.java new file mode 100644 index 0000000000..b137ff87ae --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/rest/dto/TransactionRequest.java @@ -0,0 +1,22 @@ +package com.yape.transaction.infrastructure.rest.dto; + +import lombok.Getter; +import lombok.Setter; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotNull; + +@Setter +@Getter +public class TransactionRequest { + + @NotNull(message = "Amount no puede ser null") + @DecimalMin(value = "0.01", message = "Amount debe ser mayor a 0") + @DecimalMax(value = "1000000", message = "Amount no puede ser mayor a 1,000,000") + @Digits(integer = 6, fraction = 2, message = "Amount debe tener como máximo 2 decimales") + private Double amount; + + public TransactionRequest() { + } +} diff --git a/transaction-service/src/main/java/com/yape/transaction/infrastructure/rest/exception/GlobalExceptionHandler.java b/transaction-service/src/main/java/com/yape/transaction/infrastructure/rest/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000000..7603099814 --- /dev/null +++ b/transaction-service/src/main/java/com/yape/transaction/infrastructure/rest/exception/GlobalExceptionHandler.java @@ -0,0 +1,42 @@ +package com.yape.transaction.infrastructure.rest.exception; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.context.support.DefaultMessageSourceResolvable; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class GlobalExceptionHandler { + + private ResponseEntity> buildResponse(HttpStatus status, String message) { + Map body = new HashMap<>(); + body.put("status", status.value()); + body.put("error", message); + return ResponseEntity.status(status).body(body); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationException(MethodArgumentNotValidException ex) { + String errorMessage = ex.getBindingResult() + .getFieldErrors() + .stream() + .findFirst() + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .orElse("Request inválido"); + return buildResponse(HttpStatus.BAD_REQUEST, errorMessage); + } + + @ExceptionHandler(IllegalStateException.class) + public ResponseEntity> handleIllegalStateException(IllegalStateException ex) { + return buildResponse(HttpStatus.BAD_REQUEST, ex.getMessage()); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGeneralException(Exception ex) { + return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, ex.getMessage()); + } +} diff --git a/transaction-service/src/main/resources/application.yaml b/transaction-service/src/main/resources/application.yaml new file mode 100644 index 0000000000..40ed7bbf3e --- /dev/null +++ b/transaction-service/src/main/resources/application.yaml @@ -0,0 +1,15 @@ +spring: + application: + name: transaction-service + kafka: + bootstrap-servers: ${KAFKA_SERVER:localhost:9092} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + acks: all + consumer: + group-id: transaction-service-group + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" diff --git a/transaction-service/src/test/java/com/yape/antifraud/aplication/TransactionServiceTest.java b/transaction-service/src/test/java/com/yape/antifraud/aplication/TransactionServiceTest.java new file mode 100644 index 0000000000..ae0b994cea --- /dev/null +++ b/transaction-service/src/test/java/com/yape/antifraud/aplication/TransactionServiceTest.java @@ -0,0 +1,75 @@ +package com.yape.antifraud.aplication; + +import com.yape.transaction.application.port.out.TransactionEventPublisher; +import com.yape.transaction.application.service.TransactionService; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.domain.port.out.TransactionRepository; +import com.yape.transaction.infrastructure.rest.dto.TransactionRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class TransactionServiceTest { + + private TransactionRepository repository; + private TransactionEventPublisher publisher; + private TransactionService service; + + @BeforeEach + void setup() { + repository = mock(TransactionRepository.class); + publisher = mock(TransactionEventPublisher.class); + service = new TransactionService(repository, publisher); + } + + @Test + void create_PublishTransaction() { + TransactionRequest request = new TransactionRequest(); + request.setAmount(150.0); + + Transaction tx = service.create(request); + + verify(repository, times(1)).save(any(Transaction.class)); + verify(publisher, times(1)).publish(any(Transaction.class)); + + assertThat(tx.getAmount()).isEqualTo(150.0); + assertThat(tx.getId()).isNotNull(); + } + + @Test + void getById_Transaction_WhenExists() { + UUID id = UUID.randomUUID(); + Transaction tx = new Transaction(id, 200.0); + when(repository.findById(id)).thenReturn(Optional.of(tx)); + + Transaction result = service.getById(id); + + assertThat(result).isEqualTo(tx); + verify(repository, times(1)).findById(id); + } + + @Test + void getById_Exception_WhenNotFound() { + UUID id = UUID.randomUUID(); + when(repository.findById(id)).thenReturn(Optional.empty()); + + IllegalStateException ex = assertThrows(IllegalStateException.class, () -> service.getById(id)); + assertThat(ex.getMessage()).isEqualTo("Transaction not found"); + verify(repository, times(1)).findById(id); + } + + @Test + void getAllTransactions_WhenEmpty() { + when(repository.findAll()).thenReturn(List.of()); + + IllegalStateException ex = assertThrows(IllegalStateException.class, () -> service.getAllTransactions()); + assertThat(ex.getMessage()).isEqualTo("Transactions not found"); + verify(repository, times(1)).findAll(); + } +} diff --git a/transaction-service/src/test/java/com/yape/antifraud/aplication/UpdateTransactionServiceTest.java b/transaction-service/src/test/java/com/yape/antifraud/aplication/UpdateTransactionServiceTest.java new file mode 100644 index 0000000000..a77ad19c10 --- /dev/null +++ b/transaction-service/src/test/java/com/yape/antifraud/aplication/UpdateTransactionServiceTest.java @@ -0,0 +1,54 @@ +package com.yape.antifraud.aplication; + +import com.yape.transaction.application.service.UpdateTransactionService; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.domain.model.TransactionStatus; +import com.yape.transaction.domain.port.out.TransactionRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +class UpdateTransactionServiceTest { + + private TransactionRepository repository; + private UpdateTransactionService service; + + @BeforeEach + void setup() { + repository = mock(TransactionRepository.class); + service = new UpdateTransactionService(repository); + } + + @Test + void updateStatus_Transaction() { + UUID id = UUID.randomUUID(); + Transaction transaction = new Transaction(id, 100.0); + + when(repository.findById(id)).thenReturn(Optional.of(transaction)); + + service.updateStatus(id, TransactionStatus.APPROVED); + + assertThat(transaction.getStatus()).isEqualTo(TransactionStatus.APPROVED); + verify(repository, times(1)).save(transaction); + } + + @Test + void updateStatus_WhenTransactionNotFound() { + UUID id = UUID.randomUUID(); + when(repository.findById(id)).thenReturn(Optional.empty()); + + IllegalStateException ex = assertThrows( + IllegalStateException.class, + () -> service.updateStatus(id, TransactionStatus.REJECTED) + ); + + assertThat(ex.getMessage()).isEqualTo("Transaction not found"); + verify(repository, never()).save(any()); + } +} diff --git a/transaction-service/src/test/java/com/yape/antifraud/infraestructure/db/TransactionRepositoryAdapterTest.java b/transaction-service/src/test/java/com/yape/antifraud/infraestructure/db/TransactionRepositoryAdapterTest.java new file mode 100644 index 0000000000..11ecb4a57d --- /dev/null +++ b/transaction-service/src/test/java/com/yape/antifraud/infraestructure/db/TransactionRepositoryAdapterTest.java @@ -0,0 +1,95 @@ +package com.yape.antifraud.infraestructure.db; + +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.domain.model.TransactionStatus; +import com.yape.transaction.infrastructure.db.JpaRepository; +import com.yape.transaction.infrastructure.db.TransactionRepositoryAdapter; +import com.yape.transaction.infrastructure.db.entity.TransactionEntity; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TransactionRepositoryAdapterTest { + + @Mock + private JpaRepository jpaRepository; + + private TransactionRepositoryAdapter adapter; + + private UUID id; + private Transaction transaction; + private TransactionEntity entity; + + @BeforeEach + void setup() { + adapter = new TransactionRepositoryAdapter(jpaRepository); + + id = UUID.randomUUID(); + Instant now = Instant.now(); + + transaction = new Transaction( + id, + 100.0, + TransactionStatus.PENDING, + now + ); + + entity = TransactionEntity.from(transaction); + } + + @Test + void shouldSaveTransaction() { + adapter.save(transaction); + + verify(jpaRepository).save(any(TransactionEntity.class)); + } + + @Test + void shouldFindTransactionById() { + when(jpaRepository.findById(id)).thenReturn(Optional.of(entity)); + + Optional result = adapter.findById(id); + + assertThat(result).isPresent(); + assertThat(result.get().getId()).isEqualTo(id); + assertThat(result.get().getStatus()).isEqualTo(TransactionStatus.PENDING); + } + + @Test + void shouldTransactionNotFound() { + when(jpaRepository.findById(id)).thenReturn(Optional.empty()); + + Optional result = adapter.findById(id); + + assertThat(result).isEmpty(); + } + + @Test + void shouldFindAllTransactions() { + when(jpaRepository.findAll()).thenReturn(List.of(entity)); + + List result = adapter.findAll(); + + assertThat(result).hasSize(1); + } + + @Test + void shouldNoTransactionsExist() { + when(jpaRepository.findAll()).thenReturn(List.of()); + + List result = adapter.findAll(); + + assertThat(result).isEmpty(); + } +} diff --git a/transaction-service/src/test/java/com/yape/antifraud/infraestructure/kafka/KafkaConsumerTest.java b/transaction-service/src/test/java/com/yape/antifraud/infraestructure/kafka/KafkaConsumerTest.java new file mode 100644 index 0000000000..f47cc1a805 --- /dev/null +++ b/transaction-service/src/test/java/com/yape/antifraud/infraestructure/kafka/KafkaConsumerTest.java @@ -0,0 +1,34 @@ +package com.yape.antifraud.infraestructure.kafka; + +import java.util.UUID; +import com.yape.transaction.application.port.in.UpdateTransactionUseCase; +import com.yape.transaction.domain.model.TransactionStatus; +import com.yape.transaction.infrastructure.kafka.KafkaConsumer; +import com.yape.transaction.infrastructure.kafka.events.TransactionUpdatedEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.*; + +class KafkaConsumerTest { + + private UpdateTransactionUseCase useCase; + private KafkaConsumer consumer; + + @BeforeEach + void setup() { + useCase = mock(UpdateTransactionUseCase.class); + consumer = new KafkaConsumer(useCase); + } + + @Test + void consume_CallUpdateStatus() { + UUID id = UUID.randomUUID(); + TransactionUpdatedEvent event = new TransactionUpdatedEvent(id, TransactionStatus.APPROVED); + + consumer.consume(event); + + verify(useCase, times(1)) + .updateStatus(id, TransactionStatus.APPROVED); + } +} diff --git a/transaction-service/src/test/java/com/yape/antifraud/infraestructure/rest/TransactionControllerTest.java b/transaction-service/src/test/java/com/yape/antifraud/infraestructure/rest/TransactionControllerTest.java new file mode 100644 index 0000000000..46de1451ef --- /dev/null +++ b/transaction-service/src/test/java/com/yape/antifraud/infraestructure/rest/TransactionControllerTest.java @@ -0,0 +1,71 @@ +package com.yape.antifraud.infraestructure.rest; + +import java.util.UUID; +import java.util.List; +import java.util.Arrays; + +import com.yape.transaction.infrastructure.rest.TransactionController; +import com.yape.transaction.application.port.in.TransactionUseCase; +import com.yape.transaction.domain.model.Transaction; +import com.yape.transaction.infrastructure.rest.dto.TransactionRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; + +class TransactionControllerTest { + + private TransactionUseCase useCase; + private TransactionController controller; + + @BeforeEach + void setup() { + useCase = mock(TransactionUseCase.class); + controller = new TransactionController(useCase); + } + + @Test + void create_Transaction() { + TransactionRequest request = new TransactionRequest(); + request.setAmount(100.0); + + Transaction tx = new Transaction(UUID.randomUUID(), 100.0); + when(useCase.create(request)).thenReturn(tx); + + Transaction result = controller.create(request); + + assertThat(result).isNotNull(); + assertThat(result.getAmount()).isEqualTo(100.0); + verify(useCase, times(1)).create(request); + } + + @Test + void getAll_Transactions() { + Transaction tx1 = new Transaction(UUID.randomUUID(), 50.0); + Transaction tx2 = new Transaction(UUID.randomUUID(), 75.0); + List transactions = Arrays.asList(tx1, tx2); + + when(useCase.getAllTransactions()).thenReturn(transactions); + + List result = controller.getAll(); + + assertThat(result).hasSize(2); + assertThat(result).containsExactly(tx1, tx2); + verify(useCase, times(1)).getAllTransactions(); + } + + @Test + void getById_Transaction() { + UUID id = UUID.randomUUID(); + Transaction tx = new Transaction(id, 200.0); + + when(useCase.getById(id)).thenReturn(tx); + + Transaction result = controller.getById(id); + + assertThat(result.getId()).isEqualTo(id); + assertThat(result.getAmount()).isEqualTo(200.0); + verify(useCase, times(1)).getById(id); + } +} diff --git a/transaction-service/target/classes/application.yaml b/transaction-service/target/classes/application.yaml new file mode 100644 index 0000000000..40ed7bbf3e --- /dev/null +++ b/transaction-service/target/classes/application.yaml @@ -0,0 +1,15 @@ +spring: + application: + name: transaction-service + kafka: + bootstrap-servers: ${KAFKA_SERVER:localhost:9092} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + acks: all + consumer: + group-id: transaction-service-group + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" diff --git a/transaction-service/target/classes/com/yape/transaction/TransactionApplication.class b/transaction-service/target/classes/com/yape/transaction/TransactionApplication.class new file mode 100644 index 0000000000..7623f44ddc Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/TransactionApplication.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/application/port/in/TransactionUseCase.class b/transaction-service/target/classes/com/yape/transaction/application/port/in/TransactionUseCase.class new file mode 100644 index 0000000000..8518e014f5 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/application/port/in/TransactionUseCase.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/application/port/in/UpdateTransactionUseCase.class b/transaction-service/target/classes/com/yape/transaction/application/port/in/UpdateTransactionUseCase.class new file mode 100644 index 0000000000..4e65061d8d Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/application/port/in/UpdateTransactionUseCase.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/application/port/out/TransactionEventPublisher.class b/transaction-service/target/classes/com/yape/transaction/application/port/out/TransactionEventPublisher.class new file mode 100644 index 0000000000..a85f51cfcc Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/application/port/out/TransactionEventPublisher.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/application/service/TransactionService.class b/transaction-service/target/classes/com/yape/transaction/application/service/TransactionService.class new file mode 100644 index 0000000000..d969b33a02 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/application/service/TransactionService.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/application/service/UpdateTransactionService.class b/transaction-service/target/classes/com/yape/transaction/application/service/UpdateTransactionService.class new file mode 100644 index 0000000000..e1cdf40b46 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/application/service/UpdateTransactionService.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/application/utils/Constants.class b/transaction-service/target/classes/com/yape/transaction/application/utils/Constants.class new file mode 100644 index 0000000000..39d976bd79 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/application/utils/Constants.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/domain/model/Transaction.class b/transaction-service/target/classes/com/yape/transaction/domain/model/Transaction.class new file mode 100644 index 0000000000..3038c00da0 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/domain/model/Transaction.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/domain/model/TransactionStatus.class b/transaction-service/target/classes/com/yape/transaction/domain/model/TransactionStatus.class new file mode 100644 index 0000000000..40c0e99c29 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/domain/model/TransactionStatus.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/domain/port/out/TransactionRepository.class b/transaction-service/target/classes/com/yape/transaction/domain/port/out/TransactionRepository.class new file mode 100644 index 0000000000..ac131627e6 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/domain/port/out/TransactionRepository.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/infrastructure/config/KafkaConfig.class b/transaction-service/target/classes/com/yape/transaction/infrastructure/config/KafkaConfig.class new file mode 100644 index 0000000000..7397a88562 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/infrastructure/config/KafkaConfig.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/infrastructure/db/JpaRepository.class b/transaction-service/target/classes/com/yape/transaction/infrastructure/db/JpaRepository.class new file mode 100644 index 0000000000..120e80f402 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/infrastructure/db/JpaRepository.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/infrastructure/db/TransactionRepositoryAdapter.class b/transaction-service/target/classes/com/yape/transaction/infrastructure/db/TransactionRepositoryAdapter.class new file mode 100644 index 0000000000..56c876d989 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/infrastructure/db/TransactionRepositoryAdapter.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/infrastructure/db/entity/TransactionEntity.class b/transaction-service/target/classes/com/yape/transaction/infrastructure/db/entity/TransactionEntity.class new file mode 100644 index 0000000000..d3ec0b5ab8 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/infrastructure/db/entity/TransactionEntity.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/infrastructure/kafka/KafkaConsumer.class b/transaction-service/target/classes/com/yape/transaction/infrastructure/kafka/KafkaConsumer.class new file mode 100644 index 0000000000..a1e2352633 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/infrastructure/kafka/KafkaConsumer.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/infrastructure/kafka/KafkaProducer.class b/transaction-service/target/classes/com/yape/transaction/infrastructure/kafka/KafkaProducer.class new file mode 100644 index 0000000000..93db8bafc6 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/infrastructure/kafka/KafkaProducer.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/infrastructure/kafka/events/TransactionUpdatedEvent.class b/transaction-service/target/classes/com/yape/transaction/infrastructure/kafka/events/TransactionUpdatedEvent.class new file mode 100644 index 0000000000..1074c9aa6d Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/infrastructure/kafka/events/TransactionUpdatedEvent.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/infrastructure/rest/TransactionController.class b/transaction-service/target/classes/com/yape/transaction/infrastructure/rest/TransactionController.class new file mode 100644 index 0000000000..be534ef673 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/infrastructure/rest/TransactionController.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/infrastructure/rest/dto/TransactionRequest.class b/transaction-service/target/classes/com/yape/transaction/infrastructure/rest/dto/TransactionRequest.class new file mode 100644 index 0000000000..b6e3ba0dd0 Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/infrastructure/rest/dto/TransactionRequest.class differ diff --git a/transaction-service/target/classes/com/yape/transaction/infrastructure/rest/exception/GlobalExceptionHandler.class b/transaction-service/target/classes/com/yape/transaction/infrastructure/rest/exception/GlobalExceptionHandler.class new file mode 100644 index 0000000000..79b359f52b Binary files /dev/null and b/transaction-service/target/classes/com/yape/transaction/infrastructure/rest/exception/GlobalExceptionHandler.class differ