Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions .idea/app-nodejs-codechallenge.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions .idea/compiler.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions .idea/jarRepositories.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 83 additions & 0 deletions anti-fraud-service/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.yape</groupId>
<artifactId>anti-fraud-service</artifactId>
<version>1.0.0</version>

<properties>
<java.version>21</java.version>
<spring.boot.version>3.2.1</spring.boot.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.yape.antifraud.aplication;

import com.yape.antifraud.kafka.models.TransactionReqEvent;

public interface FraudDecisionService {

void analyze(TransactionReqEvent transaction);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.yape.antifraud.aplication;

import com.yape.antifraud.kafka.models.TransactionRespEvent;

public interface ProducerService {

void sendMessage(TransactionRespEvent transaction);
}
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> producerFactory() {
Map<String, Object> 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<String, Object> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}

@Bean
public ConsumerFactory<String, TransactionReqEvent> consumerFactory() {

JsonDeserializer<TransactionReqEvent> deserializer = new JsonDeserializer<>(TransactionReqEvent.class);
deserializer.addTrustedPackages("*");
deserializer.setRemoveTypeHeaders(true);

Map<String, Object> 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<String, TransactionReqEvent> kafkaListenerContainerFactory() {

ConcurrentKafkaListenerContainerFactory<String, TransactionReqEvent> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
return factory;
}
}
Original file line number Diff line number Diff line change
@@ -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 ? "❌" : "⏳");
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Object> kafkaTemplate;

public TransactionProducer(KafkaTemplate<String, Object> 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());
}
}
Loading