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
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ You must have two resources:
}
```

## Optional
## Optional[app-transaction-report-service](app-transaction-report-service)

You can use any approach to store transaction data but you should consider that we may deal with high volume scenarios where we have a huge amount of writes and reads for the same data at the same time. How would you tackle this requirement?

Expand All @@ -80,3 +80,47 @@ You can use Graphql;
When you finish your challenge, after forking a repository, you **must** open a pull request to our repository. There are no limitations to the implementation, you can follow the programming paradigm, modularization, and style that you feel is the most appropriate solution.

If you have any questions, please let us know.




# Resumen de Implementación

## Endpoints Implementados

### 1. **Gateway de Orquestación**

* `POST /api/transactions` → Crea transacción (proxy hacia Transaction Service)
* `GET /api/transactions/{id}` → Consulta transacción


### 2. **Anti-Fraud Listener (Kafka)**

* Consumidor del tópico: `transactions.validated`
* Procesa validación antifraude y actualiza estado de transacción

### 3. **Producer de Eventos**

* Tópico: `transactions.created`
* Publicación cuando se crea una transacción

---

## Tecnologías Implementadas

| Tecnología | Uso |
| -------------------------------- | ----------------------------------------------------- |
| **Spring Boot 3.3.5** | Core del proyecto |
| **Spring WebFlux** | API reactiva y backpressure |
| **Spring Cloud Gateway 2023.x** | API Gateway reactivo |
| **Resilience4J Circuit Breaker** | Protección ante fallos en downstream |
| **Redis Rate Limiting** | Límite de requests por IP |
| **Apache Kafka** | Broker de eventos |
| **Reactor Kafka** | Publisher/Subscriber reactivo |
| **Schema Registry (Confluent)** | Gestión y versionamiento de Avro schemas |
| **Avro** | Serialización eficiente binaria para eventos |
| **Docker Compose** | Orquestación de Kafka + Registry + Redis |
| **RecordNameStrategy** | Estrategia recomendada para versionamiento de schemas |

---

2 changes: 2 additions & 0 deletions api-transaction-gateway/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf
33 changes: 33 additions & 0 deletions api-transaction-gateway/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/

### VS Code ###
.vscode/
82 changes: 82 additions & 0 deletions api-transaction-gateway/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?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.3.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<groupId>com.pe.yape.service.challenge</groupId>
<artifactId>api-transaction-gateway</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>API Transaction Gateway Service Application</description>
<url/>

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

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>


</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.pe.yape.service.challenge.transactions;

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

@SpringBootApplication
public class ApiTransactionGateway {

public static void main(String[] args) {
SpringApplication.run(ApiTransactionGateway.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.pe.yape.service.challenge.transactions.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;

import java.time.Duration;

@Configuration
public class WebClientConfig {

@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder()
.clientConnector(
new ReactorClientHttpConnector(
HttpClient.create()
.responseTimeout(Duration.ofSeconds(5))
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.pe.yape.service.challenge.transactions.controller;

import com.pe.yape.service.challenge.transactions.httpclient.dto.*;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@RestController
@RequestMapping("/api/transactions")
public class TransactionGatewayController {

private final WebClient client;

public TransactionGatewayController(WebClient.Builder builder) {
this.client = builder.build();
}

@GetMapping("/{transactionExternalId}")
@CircuitBreaker(name = "transactionServices", fallbackMethod = "fallbackTransaction")
@Retry(name = "transactionRetry")
@TimeLimiter(name = "transactionTimeout")
public Mono<SearchTransactionResponse> getById(@PathVariable String transactionExternalId) {
return client.get()
.uri("http://localhost:8085/api/transactions-report/transactions/{id}", transactionExternalId)
.retrieve()
.bodyToMono(SearchTransactionResponse.class);
}

@PostMapping("/")
@CircuitBreaker(name = "transactionServices", fallbackMethod = "fallbackCreateTransaction")
@Retry(name = "transactionRetry")
@TimeLimiter(name = "transactionTimeout")
public Mono<CreateTransactionResponse> create(@RequestBody CreateTransactionRequest request) {

return client.post()
.uri("http://localhost:8080/api/transactions/")
.bodyValue(request)
.retrieve()
.bodyToMono(CreateTransactionResponse.class);
}


public Mono<SearchTransactionResponse> fallbackTransaction(String id, Throwable ex) {
return Mono.just(new SearchTransactionResponse(
id,
new TransactionType("UNKNOWN"),
new TransactionStatus("FAILED"),
new BigDecimal("0"),
LocalDateTime.now()
));
}

public Mono<CreateTransactionResponse> fallbackCreateTransaction(CreateTransactionRequest request,
Throwable ex) {
return Mono.just(new CreateTransactionResponse("none"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pe.yape.service.challenge.transactions.httpclient.dto;

public record CreateTransactionRequest(String accountExternalIdDebit,
String accountExternalIdCredit,
Integer transferTypeId,
Double value) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.pe.yape.service.challenge.transactions.httpclient.dto;

public record CreateTransactionResponse(String transactionExternalId) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.pe.yape.service.challenge.transactions.httpclient.dto;

import java.math.BigDecimal;
import java.time.LocalDateTime;

public record SearchTransactionResponse(String transactionExternalId, TransactionType transactionType,
TransactionStatus transactionStatus, BigDecimal value,
LocalDateTime createdAt) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.pe.yape.service.challenge.transactions.httpclient.dto;

public record TransactionStatus(String name) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.pe.yape.service.challenge.transactions.httpclient.dto;

public record TransactionType(String name) {
}

32 changes: 32 additions & 0 deletions api-transaction-gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
server:
port: 9000
spring:
redis:
host: localhost
port: 6379

resilience4j:
circuitbreaker:
instances:
transactionServices:
sliding-window-size: 6
permitted-number-of-calls-in-half-open-state: 2
failure-rate-threshold: 50
wait-duration-in-open-state: 10s

retry:
instances:
transactionRetry:
max-attempts: 3
wait-duration: 2s

timelimiter:
instances:
transactionTimeout:
timeout-duration: 4s


logging:
level:
root: INFO
org.springframework.web: DEBUG
2 changes: 2 additions & 0 deletions app-anti-fraud-service/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf
33 changes: 33 additions & 0 deletions app-anti-fraud-service/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/

### VS Code ###
.vscode/
Loading