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
17 changes: 17 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,20 @@ dist

# TernJS port file
.tern-port

# Java / Maven
target/
*.class
*.jar
*.war
*.ear
.settings/
.project
.classpath
.idea/
*.iml
.factorypath

# OS
.DS_Store
Thumbs.db
488 changes: 444 additions & 44 deletions README.md

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions antifraud-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Stage 1: Build
FROM maven:3.9-eclipse-temurin-17 AS build

WORKDIR /app

# Copy POM first for dependency caching
COPY pom.xml .
RUN mvn dependency:go-offline -B

# Copy source code and build
COPY src ./src
RUN mvn clean package -DskipTests -B

# Stage 2: Run
FROM eclipse-temurin:17-jre-jammy

WORKDIR /app

RUN groupadd -r appgroup && useradd -r -g appgroup appuser

COPY --from=build /app/target/*.jar app.jar

RUN chown -R appuser:appgroup /app

USER appuser

EXPOSE 8081

ENTRYPOINT ["java", "-jar", "app.jar"]
135 changes: 135 additions & 0 deletions antifraud-service/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>

<groupId>com.yape</groupId>
<artifactId>antifraud-service</artifactId>
<version>1.0.0</version>
<name>antifraud-service</name>
<description>Anti-fraud microservice for Yape transaction validation</description>

<properties>
<java.version>17</java.version>
<testcontainers.version>1.19.7</testcontainers.version>
</properties>

<dependencies>
<!-- Spring Boot Core (no web) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<!-- Spring Data JPA -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<!-- Spring Kafka -->
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>

<!-- Flyway -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>

<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!-- Jackson Databind -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

<!-- Jackson Java Time module for LocalDateTime serialization -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

<!-- Testing -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka-test</artifactId>
<scope>test</scope>
</dependency>

<!-- Testcontainers -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.yape.antifraud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* Main entry point for the Anti-fraud microservice.
* This service consumes transaction events from Kafka, evaluates them
* against anti-fraud rules, and publishes the evaluation result.
*
* No REST endpoints are exposed - communication is event-driven via Kafka.
*/
@SpringBootApplication
public class AntifraudServiceApplication {

public static void main(String[] args) {
SpringApplication.run(AntifraudServiceApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.yape.antifraud.application.usecase;

import com.yape.antifraud.domain.port.in.EvaluateTransactionUseCase;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.util.UUID;

@Service
@Slf4j
public class EvaluateTransactionUseCaseImpl implements EvaluateTransactionUseCase {

private static final BigDecimal FRAUD_THRESHOLD = new BigDecimal("1000");

@Override
public String evaluate(UUID transactionId, BigDecimal value) {
log.info("Evaluating transaction {} with value {}", transactionId, value);

String status;
if (value.compareTo(FRAUD_THRESHOLD) > 0) {
status = "REJECTED";
log.warn("Transaction {} REJECTED - value {} exceeds threshold {}", transactionId, value, FRAUD_THRESHOLD);
} else {
status = "APPROVED";
log.info("Transaction {} APPROVED - value {} within threshold {}", transactionId, value, FRAUD_THRESHOLD);
}

return status;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.yape.antifraud.domain.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.UUID;

/**
* Pure domain entity representing an evaluated transaction.
* No JPA or infrastructure annotations - this belongs to the domain layer.
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EvaluatedTransaction {

private UUID transactionId;
private String status;
private LocalDateTime evaluatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.yape.antifraud.domain.port.in;

import java.math.BigDecimal;
import java.util.UUID;

/**
* Input port for evaluating a transaction against anti-fraud rules.
* Returns "APPROVED" if the transaction passes validation, or "REJECTED" otherwise.
*/
public interface EvaluateTransactionUseCase {

/**
* Evaluates a transaction based on its value.
*
* @param transactionId the unique identifier of the transaction
* @param value the monetary value of the transaction
* @return "APPROVED" if value <= 1000, "REJECTED" if value > 1000
*/
String evaluate(UUID transactionId, BigDecimal value);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.yape.antifraud.domain.port.out;

import com.yape.antifraud.domain.model.EvaluatedTransaction;

import java.util.Optional;
import java.util.UUID;

/**
* Output port for persisting evaluated transactions.
* This interface is defined in the domain layer and implemented by the infrastructure layer.
*/
public interface EvaluatedTransactionRepository {

/**
* Checks if a transaction has already been evaluated (idempotency check).
*
* @param transactionId the transaction identifier
* @return true if the transaction has already been evaluated
*/
boolean existsByTransactionId(UUID transactionId);

/**
* Persists an evaluated transaction.
*
* @param evaluatedTransaction the evaluated transaction to save
*/
void save(EvaluatedTransaction evaluatedTransaction);

/**
* Finds an evaluated transaction by its transaction ID.
*
* @param transactionId the transaction identifier
* @return the evaluated transaction if found
*/
Optional<EvaluatedTransaction> findByTransactionId(UUID transactionId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.yape.antifraud.domain.port.out;

import java.util.UUID;

/**
* Output port for publishing transaction status updates.
* Implemented by the Kafka producer in the infrastructure layer.
*/
public interface TransactionStatusPublisher {

/**
* Publishes a status update for a given transaction.
*
* @param transactionId the transaction identifier
* @param status the evaluation status ("APPROVED" or "REJECTED")
*/
void publishStatusUpdate(UUID transactionId, String status);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.yape.antifraud.infrastructure.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JacksonConfig {

@Bean
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
return mapper;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.yape.antifraud.infrastructure.kafka;

import org.apache.kafka.clients.admin.NewTopic;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.config.TopicBuilder;

/**
* Kafka topic configuration.
* Ensures required topics are created on application startup.
*/
@Configuration
public class KafkaConfig {

@Bean
public NewTopic transactionCreatedTopic() {
return TopicBuilder.name("transaction.created")
.partitions(3)
.replicas(1)
.build();
}

@Bean
public NewTopic transactionStatusUpdatedTopic() {
return TopicBuilder.name("transaction.status.updated")
.partitions(3)
.replicas(1)
.build();
}
}
Loading