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
163 changes: 113 additions & 50 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,82 +1,145 @@
# Yape Code Challenge :rocket:
# Reto Yape

Our code challenge will let you marvel us with your Jedi coding skills :smile:.
## Arquitectura

Don't forget that the proper way to submit your work is to fork the repo and create a PR :wink: ... have fun !!

- [Problem](#problem)
- [Tech Stack](#tech_stack)
- [Send us your challenge](#send_us_your_challenge)
```
┌─────────────────────────────────────────────────────────────────┐
│ transaction-service │
│ │
│ REST API ──► CreateTransactionUseCase ──► DB (PENDING) │
│ │ │
│ └──► Kafka: transaction.created │
│ │
│ Kafka Consumer ◄── transaction.status.updated │
│ │ │
│ └──► UpdateTransactionStatusUseCase ──► DB (update) │
└─────────────────────────────────────────────────────────────────┘
▲ │
│ ▼ Kafka
┌─────────────────────────────────────────────────────────────────┐
│ anti-fraud-service │
│ │
│ Kafka Consumer ◄── transaction.created │
│ │ │
│ └──► EvaluateTransactionUseCase │
│ value > 1000 → REJECTED │
│ value ≤ 1000 → APPROVED │
│ │ │
│ └──► Kafka: transaction.status.updated │
└─────────────────────────────────────────────────────────────────┘
```

# Problem
## Estructura de paquetes - Arquitectura Hexagonal

Every time a financial transaction is created it must be validated by our anti-fraud microservice and then the same service sends a message back to update the transaction status.
For now, we have only three transaction statuses:
```
src/main/java/com/yape/transaction/
├── domain/
│ ├── model/ # Entidades y Value Objects (núcleo del dominio)
│ │ ├── Transaction.java
│ │ ├── TransactionStatus.java
│ │ └── TransactionType.java
│ └── port/
│ ├── in/ # Casos de uso (interfaces de entrada)
│ │ ├── CreateTransactionUseCase.java
│ │ ├── GetTransactionUseCase.java
│ │ └── UpdateTransactionStatusUseCase.java
│ └── out/ # Puertos de salida (contratos de infraestructura)
│ ├── TransactionRepository.java
│ └── TransactionEventPublisher.java
├── application/
│ └── usecase/ # Implementaciones de casos de uso
│ ├── CreateTransactionService.java
│ ├── GetTransactionService.java
│ └── UpdateTransactionStatusService.java
└── infrastructure/
├── adapter/
│ ├── in/
│ │ ├── rest/ # Controllers REST (adaptador de entrada)
│ │ └── messaging/ # Kafka consumers (adaptador de entrada)
│ └── out/
│ ├── persistence/ # JPA adapters (adaptador de salida)
│ └── messaging/ # Kafka producers (adaptador de salida)
└── config/ # Configuración de Spring / Kafka
```

<ol>
<li>pending</li>
<li>approved</li>
<li>rejected</li>
</ol>
## Cómo levantar el proyecto

Every transaction with a value greater than 1000 should be rejected.
### Prerrequisitos
- Docker & Docker Compose
- Java 17+
- Maven 3.9+

```mermaid
flowchart LR
Transaction -- Save Transaction with pending Status --> transactionDatabase[(Database)]
Transaction --Send transaction Created event--> Anti-Fraud
Anti-Fraud -- Send transaction Status Approved event--> Transaction
Anti-Fraud -- Send transaction Status Rejected event--> Transaction
Transaction -- Update transaction Status event--> transactionDatabase[(Database)]
### Levantar infraestructura + servicios
```bash
docker-compose up -d
```

# Tech Stack
### Solo infraestructura (para desarrollo local)
```bash
docker-compose up -d postgres kafka zookeeper kafka-ui
```

<ol>
<li>Node. You can use any framework you want (i.e. Nestjs with an ORM like TypeOrm or Prisma) </li>
<li>Any database</li>
<li>Kafka</li>
</ol>
### Ejecutar servicios localmente
```bash
# Terminal 1 - Transaction Service
cd transaction-service
mvn spring-boot:run

We do provide a `Dockerfile` to help you get started with a dev environment.
# Terminal 2 - Anti-Fraud Service
cd anti-fraud-service
mvn spring-boot:run
```

You must have two resources:
## API Endpoints

1. Resource to create a transaction that must containt:
### Crear transacción
```http
POST http://localhost:8080/api/v1/transactions
Content-Type: application/json

```json
{
"accountExternalIdDebit": "Guid",
"accountExternalIdCredit": "Guid",
"accountExternalIdDebit": "3a0c1b2d-4e5f-6789-abcd-ef0123456789",
"accountExternalIdCredit": "7f8e9d0c-1b2a-3456-789a-bcdef0123456",
"tranferTypeId": 1,
"value": 120
}
```

2. Resource to retrieve a transaction
### Recuperar transacción
```http
GET http://localhost:8080/api/v1/transactions/{transactionExternalId}
```

### Respuesta esperada
```json
{
"transactionExternalId": "Guid",
"transactionType": {
"name": ""
},
"transactionStatus": {
"name": ""
},
"transactionExternalId": "uuid",
"transactionType": { "name": "Transfer" },
"transactionStatus": { "name": "APPROVED" },
"value": 120,
"createdAt": "Date"
"createdAt": "2024-01-15T10:30:00"
}
```

## Optional
## Tópicos Kafka

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?
| Tópico | Publicado por | Consumido por |
|-------------------------------|-----------------------|-----------------------|
| `transaction.created` | transaction-service | anti-fraud-service |
| `transaction.status.updated` | anti-fraud-service | transaction-service |

You can use Graphql;
## Reglas de negocio

# Send us your challenge
- Toda transacción inicia con estado **PENDING**
- Anti-fraud aprueba si `value <= 1000`
- Anti-fraud rechaza si `value > 1000`

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.
## Tests

If you have any questions, please let us know.
```bash
# Transaction service
cd transaction-service && mvn test

# Anti-fraud service
cd anti-fraud-service && mvn test
```
12 changes: 12 additions & 0 deletions anti-fraud-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM maven:3.9-eclipse-temurin-17-alpine AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn clean package -DskipTests

FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY --from=build /app/target/*.jar app.jar
EXPOSE 8081
ENTRYPOINT ["java", "-jar", "app.jar"]
63 changes: 63 additions & 0 deletions anti-fraud-service/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?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.0</version>
<relativePath/>
</parent>

<groupId>com.yape</groupId>
<artifactId>anti-fraud-service</artifactId>
<version>1.0.0</version>
<name>anti-fraud-service</name>

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

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

<!-- Spring Boot base (necesario para el contexto) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

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

<!-- Test -->
<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>
</dependencies>

<build>
<plugins>
<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,12 @@
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,35 @@
package com.yape.antifraud.application.usecase;

import com.yape.antifraud.domain.model.FraudEvaluation;
import com.yape.antifraud.domain.port.in.EvaluateTransactionUseCase;
import com.yape.antifraud.domain.port.out.FraudResultPublisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class EvaluateTransactionService implements EvaluateTransactionUseCase {

private static final Logger log = LoggerFactory.getLogger(EvaluateTransactionService.class);

private final FraudResultPublisher fraudResultPublisher;

public EvaluateTransactionService(FraudResultPublisher fraudResultPublisher) {
this.fraudResultPublisher = fraudResultPublisher;
}

@Override
public FraudEvaluation execute(Command command) {
FraudEvaluation evaluation = new FraudEvaluation(
command.transactionExternalId(),
command.value()
).evaluate();

log.info("Transaction {} evaluated as {} (value: {})",
command.transactionExternalId(), evaluation.getResult(), command.value());

fraudResultPublisher.publishResult(evaluation);

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

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

public class FraudEvaluation {

private static final BigDecimal MAX_ALLOWED_VALUE = BigDecimal.valueOf(1000);

private final UUID transactionExternalId;
private final BigDecimal value;
private FraudResult result;

public FraudEvaluation(UUID transactionExternalId, BigDecimal value) {
this.transactionExternalId = transactionExternalId;
this.value = value;
}

/**
* Core business rule: transactions over 1000 are rejected
* This is a pure domain method — no infrastructure concerns
*/
public FraudEvaluation evaluate() {
this.result = value.compareTo(MAX_ALLOWED_VALUE) > 0
? FraudResult.REJECTED
: FraudResult.APPROVED;
return this;
}

public UUID getTransactionExternalId() { return transactionExternalId; }
public BigDecimal getValue() { return value; }
public FraudResult getResult() { return result; }
public boolean isApproved() { return FraudResult.APPROVED.equals(result); }
public boolean isRejected() { return FraudResult.REJECTED.equals(result); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.yape.antifraud.domain.model;

public enum FraudResult {
APPROVED,
REJECTED
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.yape.antifraud.domain.port.in;

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

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

public interface EvaluateTransactionUseCase {

FraudEvaluation execute(Command command);

record Command(UUID transactionExternalId, BigDecimal value) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.yape.antifraud.domain.port.out;

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

public interface FraudResultPublisher {

void publishResult(FraudEvaluation evaluation);
}
Loading