diff --git a/.gitignore b/.gitignore
index 67045665db..32bf344217 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,104 +1,37 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-lerna-debug.log*
-
-# Diagnostic reports (https://nodejs.org/api/report.html)
-report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
-
-# Runtime data
-pids
-*.pid
-*.seed
-*.pid.lock
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
-
-# Coverage directory used by tools like istanbul
-coverage
-*.lcov
-
-# nyc test coverage
-.nyc_output
-
-# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
-
-# Bower dependency directory (https://bower.io/)
-bower_components
-
-# node-waf configuration
-.lock-wscript
-
-# Compiled binary addons (https://nodejs.org/api/addons.html)
-build/Release
-
-# Dependency directories
-node_modules/
-jspm_packages/
-
-# TypeScript v1 declaration files
-typings/
-
-# TypeScript cache
-*.tsbuildinfo
-
-# Optional npm cache directory
-.npm
-
-# Optional eslint cache
-.eslintcache
-
-# Microbundle cache
-.rpt2_cache/
-.rts2_cache_cjs/
-.rts2_cache_es/
-.rts2_cache_umd/
-
-# Optional REPL history
-.node_repl_history
-
-# Output of 'npm pack'
-*.tgz
-
-# Yarn Integrity file
-.yarn-integrity
-
-# dotenv environment variables file
-.env
-.env.test
-
-# parcel-bundler cache (https://parceljs.org/)
-.cache
-
-# Next.js build output
-.next
-
-# Nuxt.js build / generate output
-.nuxt
-dist
-
-# Gatsby files
-.cache/
-# Comment in the public line in if your project uses Gatsby and *not* Next.js
-# https://nextjs.org/blog/next-9-1#public-directory-support
-# public
-
-# vuepress build output
-.vuepress/dist
-
-# Serverless directories
-.serverless/
-
-# FuseBox cache
-.fusebox/
-
-# DynamoDB Local files
-.dynamodb/
-
-# TernJS port file
-.tern-port
+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/
+
+.mvn
+
+.idea
\ No newline at end of file
diff --git a/Dockerfile.antifraud b/Dockerfile.antifraud
new file mode 100644
index 0000000000..bc3b1e30b8
--- /dev/null
+++ b/Dockerfile.antifraud
@@ -0,0 +1,27 @@
+FROM maven:3.9.12-amazoncorretto-21-alpine AS build
+
+WORKDIR /antifraud
+
+COPY /YapeChallenge/pom.xml .
+
+COPY /YapeChallenge/TransactionManager/pom.xml ./TransactionManager/pom.xml
+COPY /YapeChallenge/TransactionManager/src ./TransactionManager/src
+
+COPY /YapeChallenge/AntiFraudManager/pom.xml ./AntiFraudManager/pom.xml
+COPY /YapeChallenge/AntiFraudManager/src ./AntiFraudManager/src
+
+
+COPY /YapeChallenge/SharedModel/pom.xml ./SharedModel/pom.xml
+COPY /YapeChallenge/SharedModel/src ./SharedModel/src
+
+RUN mvn clean package -pl AntiFraudManager -am -DskipTests=true
+
+FROM eclipse-temurin:21-jdk
+
+WORKDIR /antifraud
+
+COPY --from=build /antifraud/AntiFraudManager/target/antifraud-1.0.0.jar .
+
+EXPOSE 8081
+
+ENTRYPOINT ["java", "-jar", "antifraud-1.0.0.jar"]
\ No newline at end of file
diff --git a/Dockerfile.transaction b/Dockerfile.transaction
new file mode 100644
index 0000000000..7d91a139a1
--- /dev/null
+++ b/Dockerfile.transaction
@@ -0,0 +1,31 @@
+FROM maven:3.9.12-eclipse-temurin-21-alpine AS build
+
+WORKDIR /transaction
+
+COPY /YapeChallenge/pom.xml .
+
+COPY /YapeChallenge/TransactionManager/pom.xml ./TransactionManager/pom.xml
+COPY /YapeChallenge/TransactionManager/src ./TransactionManager/src
+
+COPY /YapeChallenge/AntiFraudManager/pom.xml ./AntiFraudManager/pom.xml
+COPY /YapeChallenge/AntiFraudManager/src ./AntiFraudManager/src
+
+
+COPY /YapeChallenge/SharedModel/pom.xml ./SharedModel/pom.xml
+COPY /YapeChallenge/SharedModel/src ./SharedModel/src
+
+RUN mvn clean package -pl TransactionManager -am -DskipTests=true
+
+FROM eclipse-temurin:21-jdk
+
+WORKDIR /transaction
+
+ENV POSTGRES_USER=
+ENV POSTGRES_PASSWORD=
+ENV POSTGRES_DB_NAME=
+
+COPY --from=build /transaction/TransactionManager/target/transaction-manager-1.0.0.jar .
+
+EXPOSE 8081
+
+ENTRYPOINT ["java", "-jar", "transaction-manager-1.0.0.jar"]
\ No newline at end of file
diff --git a/README.md b/README.md
index b067a71026..b855a82ef8 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,5 @@
# Yape Code Challenge :rocket:
-Our code challenge will let you marvel us with your Jedi coding skills :smile:.
-
-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)
-
# Problem
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.
@@ -30,53 +22,203 @@ Every transaction with a value greater than 1000 should be rejected.
Transaction -- Update transaction Status event--> transactionDatabase[(Database)]
```
-# Tech Stack
-
-
- - Node. You can use any framework you want (i.e. Nestjs with an ORM like TypeOrm or Prisma)
- - Any database
- - Kafka
-
-
-We do provide a `Dockerfile` to help you get started with a dev environment.
-
-You must have two resources:
-
-1. Resource to create a transaction that must containt:
-
-```json
+# Solution
+* To solve this problem, 2 microservices were created:
+ * AntiFraudManager microservice
+ * TransactionManager microservice
+* The microservices are implemented using reactive programing to ensure asynchronicity and improve their performance.
+* Additionally, the used technologies allow an horizonal scalation to increase performance.
+
+## Tech stack
+* Java 21
+* Spring Boot 3.5.9
+* Spring Web Flux
+* Postgres 18
+* Kafka
+* Redis 8.4
+* Cassandra (Additional for possible high volume read/write scenarios)
+
+### Main dependencies
+* spring-boot-starter (3.5.9)
+* spring-boot-starter-webflux (3.5.9)
+* spring-kafka (3.3.11)
+* lz4-java (1.10.2)
+* reactor-kafka (1.3.25)
+* spring-boot-starter-data-r2dbc (3.5.9)
+* r2dbc-postgresql (1.1.1.RELEASE)
+* spring-boot-starter-data-redis-reactive (3.5.9)
+* spring-boot-starter-data-cassandra (3.5.9)
+
+# Microservices
+## TransactionManager
+* Allows creating transactions
+### Endpoints
+
+#### [POST] /transaction
+* Creates a transaction
+```
+[POST] /transaction (Request)
{
- "accountExternalIdDebit": "Guid",
- "accountExternalIdCredit": "Guid",
- "tranferTypeId": 1,
- "value": 120
+ "sourceAccountId": "{UUID}",
+ "destinationAccountId":"{UUID}",
+ "amount": {decimal-number}
}
```
-
-2. Resource to retrieve a transaction
-
-```json
+```
+[POST] /transaction (Response)
{
- "transactionExternalId": "Guid",
- "transactionType": {
- "name": ""
- },
- "transactionStatus": {
- "name": ""
- },
- "value": 120,
- "createdAt": "Date"
+ "message": "Transaction created successfully",
+ "transactionId": "{UUID}"
}
```
-## Optional
+#### [GET] /transaction/{transactionId}
+* Returns the details from a transaction
+```
+[GET] /transaction/{transactionId} (Response)
+{
+ "id": "{UUID}",
+ "sourceAccountId": "{UUID}",
+ "destinationAccountId": "{UUID}",
+ "status": "REJECTED" | "PENDING" | "APPROVED",
+ "type": "DEPOSIT",
+ "amount": {decimal-number},
+ "createdAt": {time-in-millis}
+}
+```
-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?
+#### [GET] /transaction/account/{transactionId}
+* Retrieves the transactions for a given account
+ * _**Additional endpoint to validate Cassandra usage**_
+```
+[GET] /transaction/account/{transactionId} (Response)
+[
+ {
+ "id": "{UUID}",
+ "sourceAccountId": "{UUID}",
+ "destinationAccountId": "{UUID}",
+ "status": "REJECTED" | "PENDING" | "APPROVED",
+ "type": "DEPOSIT",
+ "amount": {decimal-number},
+ "createdAt": {time-in-millis}
+ },
+ ...
+]
+```
-You can use Graphql;
+### Flow
+#### Create a Transaction
+* A transaction is created using the endpoint `[POST] /transaction`
+* The transaction, with `PENDING` status, is persisted into the `transaction` table in the PostgreSQL database
+* A transaction entry is stored in Redis (to improve performance when retrieving transaction details)
+* The transaction is also stored into the `transaction_by_account` table in the Cassandra database
+ * Using the field `sourceAccountId` as the partition key and the fields `createdAt` and `id` for the clustering columns
+* A `transaction event` is sent to Kafka topic `event`
+ * The `transaction event` have the following fields:
+ * transactionId (String)
+ * amount (float)
+
+#### Consume response events
+* A Kafka consumer is subscribed to the Kafka topic `event-response`
+ * The AntiFraudManager microservice publishes the transaction event response validations into this topic
+ * The `transaction event response` have the following fields:
+ * transactionId (String)
+ * isValid (Boolean)
+* Once a `transaction event response` is consumed,the `transaction` will be updated according to the `isValid` value:
+ * `APPROVED` if `true`
+ * `REJECTED` if `false`
+* The transaction will be updated in:
+ * The `transaction` table in PostgreSQL database
+ * The entry in Redis
+ * The `transaction_by_account` table in the Cassandra database
+
+#### Get transaction details
+* The transaction details are requested using the endpoint `[POST] /transaction/{transactionId}`
+* If the transaction is available in Redis, it will be retrieved. Otherwise, it will be retrieved from the database.
+
+#### Get transaction details by account
+* The transactions by a given account are requested using the endpoint `[POST] /transaction/account/{accountId}`
+* They will be retrieved from the required partition based on the `accountId` value from the Cassandra database.
+ * The table `transaction_by_account` was build specifically for this query.
+
+## AntiFraud
+### Flow
+#### Consume events
+* A Kafka consumer is subscribed to the Kafka topic `event`
+* Once the transaction event is consumed from Kafka, the `amount` field will be validated to set the response field `isValid`.
+ * If the amount is lower or equal than 1000 it will be `true` otherwise it will be `false`
+* A `transaction event response` is sent to Kafka topic `event-response`
+ * The `transaction event response` have the following fields:
+ * transactionId (String)
+ * isValid (Boolean)
+
+# Build and run
+## Requirements
+* Install Docker for your current OS (https://www.docker.com/get-started/)
+## Build Steps
+* Clone the repository and set into the `yape-challenge` directory
+* Set the following environment variables (credentials for the database):
+ * POSTGRES_DB_NAME
+ * POSTGRES_USER
+ * POSTGRES_PASSWORD
+```
+export POSTGRES_DB_NAME={db-name}
+export POSTGRES_DB_USER={db-user}
+export POSTGRES_DB_PASSWORD={db-password}
+```
+* In the `/yape-challenge/Dockerfile.transaction` file update the following values:
+```
+ENV POSTGRES_USER={db-user}
+ENV POSTGRES_PASSWORD={db-password}
+ENV POSTGRES_DB_NAME={db-name}
+```
+* Execute the docker command `docker compose up -d`
+* Wait until all the Docker containers are running
+```
+~/yape-challenge$ docker compose up -d
+WARN[0000] No services to build
+[+] up 8/8
+ ✔ Container cassandra Healthy 0.6s
+ ✔ Container postgres Running 0.0s
+ ✔ Container redis Running 0.0s
+ ✔ Container zookeeper Running 0.0s
+ ✔ Container kafka Running 0.0s
+ ✔ Container transaction Running 0.0s
+ ✔ Container antifraud Running 0.0s
+ ✔ Container cassandra-baseline Exited
+```
+## Usage
+* The TransactionManager endpoints will be available in the `localhost:8082` host
+* You can manually validate the microservices endpoints using Postman
+* Here are some examples:
+```
+[POST] http://localhost:8082/transaction (Request)
+{
+ "sourceAccountId": "8fcb1a7d-bc3a-4cf1-9077-e1eed948ac93",
+ "destinationAccountId":"8fcb1a7d-bc3a-4cf1-9077-e1eed948ac93",
+ "amount":11110.50
+}
-# Send us your challenge
+[POST] http://localhost:8082/transaction (Response)
+{
+ "message": "Transaction created successfully",
+ "transactionId": "76e3f045-674a-466a-800a-28722bd0d13b"
+}
+```
-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.
+```
+[GET] http://localhost:8082/transaction/76e3f045-674a-466a-800a-28722bd0d13b (Response)
+{
+ "id": "76e3f045-674a-466a-800a-28722bd0d13b",
+ "sourceAccountId": "8fcb1a7d-bc3a-4cf1-9077-e1eed948ac93",
+ "destinationAccountId": "8fcb1a7d-bc3a-4cf1-9077-e1eed948ac93",
+ "status": "REJECTED",
+ "type": "DEPOSIT",
+ "amount": 11110.5,
+ "createdAt": 1768181711540
+}
+```
-If you have any questions, please let us know.
+# Future improvements
+* Use Spring Cloud Gateway (https://spring.io/projects/spring-cloud-gateway) with the Eureka discovery client (https://docs.spring.io/spring-cloud-netflix/docs/current/reference/html/) to build a load balancer so multiple instances of the microservices can be initiated.
+* Define specific queries that will require huge amount of writes or reads so the required tables can be designed in Cassandra.
diff --git a/YapeChallenge/AntiFraudManager/pom.xml b/YapeChallenge/AntiFraudManager/pom.xml
new file mode 100644
index 0000000000..9dd3da50ae
--- /dev/null
+++ b/YapeChallenge/AntiFraudManager/pom.xml
@@ -0,0 +1,54 @@
+
+
+ 4.0.0
+
+ com.msucre
+ yape-challenge
+ 1.0.0
+
+
+ antifraud
+ jar
+
+ AntiFraud
+ AntiFraund Manager
+
+
+
+ com.msucre
+ shared-model
+ 1.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+
+
+ org.lz4
+ lz4-java
+
+
+
+
+
+
+
+ at.yawk.lz4
+ lz4-java
+ ${lz4-version}
+
+
+ io.projectreactor.kafka
+ reactor-kafka
+ ${react-kafka-version}
+
+
+
diff --git a/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/AntiFraudApplication.java b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/AntiFraudApplication.java
new file mode 100644
index 0000000000..aa5c8db7dd
--- /dev/null
+++ b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/AntiFraudApplication.java
@@ -0,0 +1,13 @@
+package com.msucre.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/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/application/usecase/transaction/ValidateTransactionCase.java b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/application/usecase/transaction/ValidateTransactionCase.java
new file mode 100644
index 0000000000..a74815a497
--- /dev/null
+++ b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/application/usecase/transaction/ValidateTransactionCase.java
@@ -0,0 +1,36 @@
+package com.msucre.antifraud.application.usecase.transaction;
+
+import com.msucre.antifraud.domain.service.EventService;
+import com.msucre.antifraud.domain.service.ValidateTransactionService;
+import com.msucre.shared.model.dto.event.TransactionEventResponseDto;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+
+@Component
+public class ValidateTransactionCase {
+
+ private final ValidateTransactionService validateTransactionService;
+
+ private final EventService eventService;
+
+ public ValidateTransactionCase(ValidateTransactionService validateTransactionService, EventService eventService) {
+ this.validateTransactionService = validateTransactionService;
+ this.eventService = eventService;
+ }
+
+ @PostConstruct
+ private void init() {
+ execute().subscribe();
+ }
+
+ private Flux execute() {
+ return eventService.consumeTransactionEvents()
+ .map(transactionEvent -> {
+ boolean isValid = validateTransactionService.isValid(transactionEvent);
+ return new TransactionEventResponseDto(transactionEvent.transactionId(),
+ isValid);
+ })
+ .flatMap(eventService::sendTransactionEventResponse);
+ }
+}
\ No newline at end of file
diff --git a/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/domain/service/EventService.java b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/domain/service/EventService.java
new file mode 100644
index 0000000000..a662fe71f3
--- /dev/null
+++ b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/domain/service/EventService.java
@@ -0,0 +1,15 @@
+package com.msucre.antifraud.domain.service;
+
+
+import com.msucre.shared.model.dto.event.TransactionEventDto;
+import com.msucre.shared.model.dto.event.TransactionEventResponseDto;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface EventService {
+
+ Mono sendTransactionEventResponse(
+ TransactionEventResponseDto transactionEventResponseDto);
+
+ Flux consumeTransactionEvents();
+}
diff --git a/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/domain/service/ValidateTransactionService.java b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/domain/service/ValidateTransactionService.java
new file mode 100644
index 0000000000..ed4a4bb079
--- /dev/null
+++ b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/domain/service/ValidateTransactionService.java
@@ -0,0 +1,8 @@
+package com.msucre.antifraud.domain.service;
+
+import com.msucre.shared.model.dto.event.TransactionEventDto;
+
+public interface ValidateTransactionService {
+
+ boolean isValid(TransactionEventDto transactionEventDto);
+}
\ No newline at end of file
diff --git a/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/infrastructure/kafka/configuration/KafkaConfig.java b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/infrastructure/kafka/configuration/KafkaConfig.java
new file mode 100644
index 0000000000..0c343fa534
--- /dev/null
+++ b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/infrastructure/kafka/configuration/KafkaConfig.java
@@ -0,0 +1,48 @@
+package com.msucre.antifraud.infrastructure.kafka.configuration;
+
+
+import com.msucre.shared.model.kafka.KafkaSharedConstants;
+import com.msucre.shared.model.dto.event.TransactionEventDto;
+import com.msucre.shared.model.dto.event.TransactionEventResponseDto;
+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.boot.autoconfigure.kafka.KafkaProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.core.reactive.ReactiveKafkaConsumerTemplate;
+import org.springframework.kafka.core.reactive.ReactiveKafkaProducerTemplate;
+import org.springframework.kafka.support.serializer.JsonDeserializer;
+import org.springframework.kafka.support.serializer.JsonSerializer;
+import reactor.kafka.receiver.ReceiverOptions;
+import reactor.kafka.sender.SenderOptions;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Configuration
+public class KafkaConfig {
+
+ @Bean
+ public ReactiveKafkaProducerTemplate reactiveKafkaProducerTemplate(
+ KafkaProperties properties) {
+ Map producerProperties = properties.buildProducerProperties();
+ producerProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
+ producerProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
+ return new ReactiveKafkaProducerTemplate<>(SenderOptions.create(producerProperties));
+ }
+
+ @Bean
+ public ReactiveKafkaConsumerTemplate reactiveKafkaConsumerTemplate(
+ KafkaProperties properties) {
+ Map consumerProperties = properties.buildProducerProperties();
+ consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+ consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
+ consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, KafkaSharedConstants.KAFKA_GROUP_ID);
+ consumerProperties.put(JsonDeserializer.TRUSTED_PACKAGES, KafkaSharedConstants.MODEL_TRUSTED_PACKAGE);
+ ReceiverOptions receiverOptions = ReceiverOptions.create(
+ consumerProperties).subscription(Collections.singletonList(KafkaSharedConstants.EVENT_TOPIC));
+ return new ReactiveKafkaConsumerTemplate<>(receiverOptions);
+ }
+}
diff --git a/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/infrastructure/kafka/service/KafkaEventService.java b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/infrastructure/kafka/service/KafkaEventService.java
new file mode 100644
index 0000000000..e77b92a90d
--- /dev/null
+++ b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/infrastructure/kafka/service/KafkaEventService.java
@@ -0,0 +1,46 @@
+package com.msucre.antifraud.infrastructure.kafka.service;
+
+
+import com.msucre.antifraud.domain.service.EventService;
+import com.msucre.shared.model.kafka.KafkaSharedConstants;
+import com.msucre.shared.model.dto.event.TransactionEventDto;
+import com.msucre.shared.model.dto.event.TransactionEventResponseDto;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.kafka.core.reactive.ReactiveKafkaConsumerTemplate;
+import org.springframework.kafka.core.reactive.ReactiveKafkaProducerTemplate;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@Service
+public class KafkaEventService implements EventService {
+
+ private static final Logger logger = LoggerFactory.getLogger(KafkaEventService.class);
+
+ private final ReactiveKafkaProducerTemplate kafkaProducerTemplate;
+
+ private final ReactiveKafkaConsumerTemplate kafkaConsumerTemplate;
+
+ public KafkaEventService(ReactiveKafkaProducerTemplate kafkaProducerTemplate,
+ ReactiveKafkaConsumerTemplate kafkaConsumerTemplate) {
+ this.kafkaProducerTemplate = kafkaProducerTemplate;
+ this.kafkaConsumerTemplate = kafkaConsumerTemplate;
+ }
+
+ @Override
+ public Mono sendTransactionEventResponse(final TransactionEventResponseDto transactionEventResponseDto) {
+ return kafkaProducerTemplate.send(KafkaSharedConstants.EVENT_RESPONSE_TOPIC, transactionEventResponseDto).doOnSuccess(
+ unused -> logger.debug("Sending response: [{} - {}]", transactionEventResponseDto.transactionId(),
+ transactionEventResponseDto.isValid())).then();
+ }
+
+ @Override
+ public Flux consumeTransactionEvents() {
+ return kafkaConsumerTemplate.receiveAutoAck().map(ConsumerRecord::value).doOnNext(
+ transactionEvent -> logger.debug("Transaction received: {} - {}", transactionEvent.transactionId(),
+ transactionEvent.amount()))
+ .onErrorContinue((throwable, o) -> logger.error("Error while consuming transaction event", throwable));
+ }
+}
\ No newline at end of file
diff --git a/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/infrastructure/validation/service/ValidateTransactionServiceImpl.java b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/infrastructure/validation/service/ValidateTransactionServiceImpl.java
new file mode 100644
index 0000000000..d2cc479484
--- /dev/null
+++ b/YapeChallenge/AntiFraudManager/src/main/java/com/msucre/antifraud/infrastructure/validation/service/ValidateTransactionServiceImpl.java
@@ -0,0 +1,16 @@
+package com.msucre.antifraud.infrastructure.validation.service;
+
+import com.msucre.antifraud.domain.service.ValidateTransactionService;
+import com.msucre.shared.model.dto.event.TransactionEventDto;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ValidateTransactionServiceImpl implements ValidateTransactionService {
+
+ private static final float MAX_ALLOWED_AMOUNT = 1000;
+
+ @Override
+ public boolean isValid(final TransactionEventDto transactionEventDto) {
+ return transactionEventDto.amount() <= MAX_ALLOWED_AMOUNT;
+ }
+}
\ No newline at end of file
diff --git a/YapeChallenge/AntiFraudManager/src/main/resources/application.properties b/YapeChallenge/AntiFraudManager/src/main/resources/application.properties
new file mode 100644
index 0000000000..b1ee335b43
--- /dev/null
+++ b/YapeChallenge/AntiFraudManager/src/main/resources/application.properties
@@ -0,0 +1,7 @@
+spring.application.name=AntiFraud
+server.port=8081
+
+spring.kafka.bootstrap-servers=kafka:9092
+
+#logging level
+logging.level.com.msucre.antifraud=DEBUG
\ No newline at end of file
diff --git a/YapeChallenge/AntiFraudManager/src/test/java/com/msucre/antifraud/AntiFraudApplicationTests.java b/YapeChallenge/AntiFraudManager/src/test/java/com/msucre/antifraud/AntiFraudApplicationTests.java
new file mode 100644
index 0000000000..db1cd005f3
--- /dev/null
+++ b/YapeChallenge/AntiFraudManager/src/test/java/com/msucre/antifraud/AntiFraudApplicationTests.java
@@ -0,0 +1,9 @@
+package com.msucre.antifraud;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class AntiFraudApplicationTests {
+
+}
diff --git a/YapeChallenge/SharedModel/pom.xml b/YapeChallenge/SharedModel/pom.xml
new file mode 100644
index 0000000000..01d36496e7
--- /dev/null
+++ b/YapeChallenge/SharedModel/pom.xml
@@ -0,0 +1,32 @@
+
+
+ 4.0.0
+
+ com.msucre
+ shared-model
+ jar
+ 1.0.0
+ SharedModel
+ Classes shared between microservices
+
+
+ 21
+ 3.13.0
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${compiler-plugin-version}
+
+ ${java.version}
+ ${java.version}
+
+
+
+
+
+
diff --git a/YapeChallenge/SharedModel/src/main/java/com/msucre/shared/model/dto/event/TransactionEventDto.java b/YapeChallenge/SharedModel/src/main/java/com/msucre/shared/model/dto/event/TransactionEventDto.java
new file mode 100644
index 0000000000..2435429c78
--- /dev/null
+++ b/YapeChallenge/SharedModel/src/main/java/com/msucre/shared/model/dto/event/TransactionEventDto.java
@@ -0,0 +1,4 @@
+package com.msucre.shared.model.dto.event;
+
+public record TransactionEventDto(String transactionId, float amount) {
+}
diff --git a/YapeChallenge/SharedModel/src/main/java/com/msucre/shared/model/dto/event/TransactionEventResponseDto.java b/YapeChallenge/SharedModel/src/main/java/com/msucre/shared/model/dto/event/TransactionEventResponseDto.java
new file mode 100644
index 0000000000..a4313287d0
--- /dev/null
+++ b/YapeChallenge/SharedModel/src/main/java/com/msucre/shared/model/dto/event/TransactionEventResponseDto.java
@@ -0,0 +1,4 @@
+package com.msucre.shared.model.dto.event;
+
+public record TransactionEventResponseDto(String transactionId, boolean isValid) {
+}
diff --git a/YapeChallenge/SharedModel/src/main/java/com/msucre/shared/model/kafka/KafkaSharedConstants.java b/YapeChallenge/SharedModel/src/main/java/com/msucre/shared/model/kafka/KafkaSharedConstants.java
new file mode 100644
index 0000000000..33c101f225
--- /dev/null
+++ b/YapeChallenge/SharedModel/src/main/java/com/msucre/shared/model/kafka/KafkaSharedConstants.java
@@ -0,0 +1,12 @@
+package com.msucre.shared.model.kafka;
+
+public class KafkaSharedConstants {
+
+ public static final String EVENT_RESPONSE_TOPIC = "event-response";
+
+ public static final String EVENT_TOPIC = "event";
+
+ public static final String KAFKA_GROUP_ID = "antifraud-group";
+
+ public static final String MODEL_TRUSTED_PACKAGE = "com.msucre.shared.model.dto.event";
+}
diff --git a/YapeChallenge/SharedModel/src/main/resources/application.properties b/YapeChallenge/SharedModel/src/main/resources/application.properties
new file mode 100644
index 0000000000..dfcac877c5
--- /dev/null
+++ b/YapeChallenge/SharedModel/src/main/resources/application.properties
@@ -0,0 +1 @@
+spring.application.name=SharedModel
diff --git a/YapeChallenge/TransactionManager/pom.xml b/YapeChallenge/TransactionManager/pom.xml
new file mode 100644
index 0000000000..ccce436b96
--- /dev/null
+++ b/YapeChallenge/TransactionManager/pom.xml
@@ -0,0 +1,75 @@
+
+
+ 4.0.0
+
+ com.msucre
+ yape-challenge
+ 1.0.0
+
+
+ transaction-manager
+ jar
+
+ TransactionManager
+ Transaction Manager
+
+
+ 1.1.1.RELEASE
+
+
+
+
+ com.msucre
+ shared-model
+ 1.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
+
+
+ org.springframework.kafka
+ spring-kafka
+
+
+
+
+
+ org.lz4
+ lz4-java
+
+
+
+
+
+
+
+ at.yawk.lz4
+ lz4-java
+ ${lz4-version}
+
+
+ io.projectreactor.kafka
+ reactor-kafka
+ ${react-kafka-version}
+
+
+ org.springframework.boot
+ spring-boot-starter-data-r2dbc
+
+
+ org.postgresql
+ r2dbc-postgresql
+ ${r2dbc-postgresql-version}
+
+
+ org.springframework.boot
+ spring-boot-starter-data-cassandra
+
+
+ org.springframework.boot
+ spring-boot-starter-data-redis-reactive
+
+
+
\ No newline at end of file
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/TransactionManagerApplication.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/TransactionManagerApplication.java
new file mode 100644
index 0000000000..f0d81802c3
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/TransactionManagerApplication.java
@@ -0,0 +1,17 @@
+package com.msucre.transaction;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
+
+@SpringBootApplication
+@EnableR2dbcRepositories(
+ basePackages = "com.company.transaction.infrastructure.postgres.repository"
+)
+public class TransactionManagerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(TransactionManagerApplication.class, args);
+ }
+
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/CreateTransactionCase.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/CreateTransactionCase.java
new file mode 100644
index 0000000000..0d8b73cd5c
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/CreateTransactionCase.java
@@ -0,0 +1,60 @@
+package com.msucre.transaction.application.usecase.transaction;
+
+import com.msucre.shared.model.dto.event.TransactionEventDto;
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.domain.model.type.TransactionStatus;
+import com.msucre.transaction.domain.model.type.TransactionType;
+import com.msucre.transaction.domain.repository.TransactionByAccountRepository;
+import com.msucre.transaction.domain.repository.TransactionCacheRepository;
+import com.msucre.transaction.domain.repository.TransactionRepository;
+import com.msucre.transaction.domain.service.EventService;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
+import java.util.UUID;
+
+@Component
+public class CreateTransactionCase {
+
+ private final TransactionRepository transactionRepository;
+
+ private final TransactionCacheRepository transactionCacheRepository;
+
+ private final TransactionByAccountRepository transactionByAccountRepository;
+
+ private final EventService eventService;
+
+ public CreateTransactionCase(TransactionRepository transactionRepository,
+ TransactionCacheRepository transactionCacheRepository,
+ TransactionByAccountRepository transactionByAccountRepository,
+ EventService eventService) {
+ this.transactionRepository = transactionRepository;
+ this.transactionCacheRepository = transactionCacheRepository;
+ this.transactionByAccountRepository = transactionByAccountRepository;
+ this.eventService = eventService;
+ }
+
+ public Mono execute(final Transaction transaction) {
+ return Mono.fromCallable(() -> {
+ transaction.createdAt = System.currentTimeMillis();
+ transaction.status = TransactionStatus.PENDING;
+ transaction.type = TransactionType.DEPOSIT;
+ return transaction;
+ }).flatMap(this::createAndProcessTransaction).map(createdTransaction -> createdTransaction.id);
+ }
+
+ private Mono createAndProcessTransaction(final Transaction transaction) {
+ return transactionRepository.save(transaction)
+ .doOnNext(createdTransaction ->
+ processTransaction(createdTransaction).subscribeOn(Schedulers.boundedElastic()).subscribe()
+ );
+ }
+
+ private Mono processTransaction(final Transaction transaction) {
+ return transactionCacheRepository.save(transaction).then(transactionByAccountRepository.save(transaction))
+ .then(
+ eventService.sendTransactionEvent(new TransactionEventDto(transaction.id.toString(), transaction.amount)));
+ }
+}
+
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/FindTransactionCase.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/FindTransactionCase.java
new file mode 100644
index 0000000000..db3e72a13a
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/FindTransactionCase.java
@@ -0,0 +1,27 @@
+package com.msucre.transaction.application.usecase.transaction;
+
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.domain.repository.TransactionCacheRepository;
+import com.msucre.transaction.domain.repository.TransactionRepository;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+@Component
+public class FindTransactionCase {
+
+ private final TransactionCacheRepository transactionCacheRepository;
+ private final TransactionRepository transactionRepository;
+
+
+ public FindTransactionCase(TransactionCacheRepository transactionCacheRepository,
+ TransactionRepository transactionRepository) {
+ this.transactionCacheRepository = transactionCacheRepository;
+ this.transactionRepository = transactionRepository;
+ }
+
+ public Mono execute(final String transactionId) {
+ return transactionCacheRepository.findById(transactionId).switchIfEmpty(
+ transactionRepository.findById(transactionId)
+ .flatMap(transaction -> transactionCacheRepository.save(transaction).thenReturn(transaction)));
+ }
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/FindTransactionsByAccountCase.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/FindTransactionsByAccountCase.java
new file mode 100644
index 0000000000..760626129c
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/FindTransactionsByAccountCase.java
@@ -0,0 +1,22 @@
+package com.msucre.transaction.application.usecase.transaction;
+
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.domain.repository.TransactionByAccountRepository;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+
+import java.util.UUID;
+
+@Component
+public class FindTransactionsByAccountCase {
+
+ private final TransactionByAccountRepository transactionByAccountRepository;
+
+ public FindTransactionsByAccountCase(TransactionByAccountRepository transactionByAccountRepository) {
+ this.transactionByAccountRepository = transactionByAccountRepository;
+ }
+
+ public Flux execute(String accountId) {
+ return transactionByAccountRepository.findTransactionsByAccount(UUID.fromString(accountId));
+ }
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/UpdateValidatedTransactionCase.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/UpdateValidatedTransactionCase.java
new file mode 100644
index 0000000000..5d9288cb26
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/application/usecase/transaction/UpdateValidatedTransactionCase.java
@@ -0,0 +1,50 @@
+package com.msucre.transaction.application.usecase.transaction;
+
+import com.msucre.transaction.domain.model.type.TransactionStatus;
+import com.msucre.transaction.domain.repository.TransactionByAccountRepository;
+import com.msucre.transaction.domain.repository.TransactionRepository;
+import com.msucre.transaction.domain.service.EventService;
+import com.msucre.transaction.infrastructure.redis.repository.RedisTransactionCacheRepository;
+import jakarta.annotation.PostConstruct;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@Component
+public class UpdateValidatedTransactionCase {
+
+ private final EventService eventService;
+
+ private final TransactionRepository transactionRepository;
+
+ private final RedisTransactionCacheRepository redisTransactionCacheRepository;
+
+ private final TransactionByAccountRepository transactionByAccountRepository;
+
+ public UpdateValidatedTransactionCase(EventService eventService, TransactionRepository transactionRepository,
+ RedisTransactionCacheRepository redisTransactionCacheRepository,
+ TransactionByAccountRepository transactionByAccountRepository) {
+ this.eventService = eventService;
+ this.transactionRepository = transactionRepository;
+ this.redisTransactionCacheRepository = redisTransactionCacheRepository;
+ this.transactionByAccountRepository = transactionByAccountRepository;
+ }
+
+ @PostConstruct
+ public void init() {
+ execute().subscribe();
+ }
+
+ public Flux execute() {
+ return eventService.consumeTransactionEventResponses().flatMap(
+ transactionEventResponseDto -> processTransaction(transactionEventResponseDto.transactionId(),
+ transactionEventResponseDto.isValid() ? TransactionStatus.APPROVED : TransactionStatus.REJECTED));
+ }
+
+ private Mono processTransaction(final String id, final TransactionStatus status) {
+ return transactionRepository.updateStatusById(id, status)
+ .then(redisTransactionCacheRepository.updateStatusById(id, status))
+ .then(transactionRepository.findById(id))
+ .flatMap(transaction -> transactionByAccountRepository.updateStatus(transaction, status));
+ }
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/Transaction.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/Transaction.java
new file mode 100644
index 0000000000..d03bd24aaf
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/Transaction.java
@@ -0,0 +1,23 @@
+package com.msucre.transaction.domain.model;
+
+import com.msucre.transaction.domain.model.type.TransactionStatus;
+import com.msucre.transaction.domain.model.type.TransactionType;
+
+import java.util.UUID;
+
+public class Transaction {
+
+ public UUID id;
+
+ public UUID sourceAccountId;
+
+ public UUID destinationAccountId;
+
+ public TransactionStatus status;
+
+ public TransactionType type;
+
+ public float amount;
+
+ public Long createdAt;
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/type/TransactionStatus.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/type/TransactionStatus.java
new file mode 100644
index 0000000000..2a22c9030c
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/type/TransactionStatus.java
@@ -0,0 +1,7 @@
+package com.msucre.transaction.domain.model.type;
+
+public enum TransactionStatus {
+ PENDING,
+ APPROVED,
+ REJECTED
+}
\ No newline at end of file
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/type/TransactionType.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/type/TransactionType.java
new file mode 100644
index 0000000000..06552ad96a
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/type/TransactionType.java
@@ -0,0 +1,5 @@
+package com.msucre.transaction.domain.model.type;
+
+public enum TransactionType {
+ DEPOSIT
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/ui/CreateTransactionResponse.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/ui/CreateTransactionResponse.java
new file mode 100644
index 0000000000..1ce37abdb1
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/model/ui/CreateTransactionResponse.java
@@ -0,0 +1,7 @@
+package com.msucre.transaction.domain.model.ui;
+
+import java.util.UUID;
+
+public record CreateTransactionResponse(String message, UUID transactionId) {
+
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/repository/TransactionByAccountRepository.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/repository/TransactionByAccountRepository.java
new file mode 100644
index 0000000000..a5c59bbccc
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/repository/TransactionByAccountRepository.java
@@ -0,0 +1,17 @@
+package com.msucre.transaction.domain.repository;
+
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.domain.model.type.TransactionStatus;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+public interface TransactionByAccountRepository {
+
+ Mono save(Transaction transaction);
+
+ Flux findTransactionsByAccount(UUID accountId);
+
+ Mono updateStatus(Transaction transaction, TransactionStatus status);
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/repository/TransactionCacheRepository.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/repository/TransactionCacheRepository.java
new file mode 100644
index 0000000000..936c3faae9
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/repository/TransactionCacheRepository.java
@@ -0,0 +1,14 @@
+package com.msucre.transaction.domain.repository;
+
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.domain.model.type.TransactionStatus;
+import reactor.core.publisher.Mono;
+
+public interface TransactionCacheRepository {
+
+ Mono save(Transaction transaction);
+
+ Mono updateStatusById(String id, TransactionStatus status);
+
+ Mono findById(String id);
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/repository/TransactionRepository.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/repository/TransactionRepository.java
new file mode 100644
index 0000000000..f698a0f412
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/repository/TransactionRepository.java
@@ -0,0 +1,16 @@
+package com.msucre.transaction.domain.repository;
+
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.domain.model.type.TransactionStatus;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+public interface TransactionRepository {
+
+ Mono save(Transaction transaction);
+
+ Mono updateStatusById(String id, TransactionStatus status);
+
+ Mono findById(String id);
+}
\ No newline at end of file
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/service/EventService.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/service/EventService.java
new file mode 100644
index 0000000000..4c1159039c
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/domain/service/EventService.java
@@ -0,0 +1,14 @@
+package com.msucre.transaction.domain.service;
+
+
+import com.msucre.shared.model.dto.event.TransactionEventDto;
+import com.msucre.shared.model.dto.event.TransactionEventResponseDto;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public interface EventService {
+
+ Mono sendTransactionEvent(TransactionEventDto transactionEventDto);
+
+ Flux consumeTransactionEventResponses();
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/entity/TransactionByAccountEntity.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/entity/TransactionByAccountEntity.java
new file mode 100644
index 0000000000..f094dec4a7
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/entity/TransactionByAccountEntity.java
@@ -0,0 +1,34 @@
+package com.msucre.transaction.infrastructure.cassandra.entity;
+
+import com.msucre.transaction.domain.model.type.TransactionStatus;
+import com.msucre.transaction.domain.model.type.TransactionType;
+import org.springframework.data.cassandra.core.cql.Ordering;
+import org.springframework.data.cassandra.core.cql.PrimaryKeyType;
+import org.springframework.data.cassandra.core.mapping.Column;
+import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn;
+import org.springframework.data.cassandra.core.mapping.Table;
+
+import java.util.UUID;
+
+
+@Table("transaction_by_account")
+public class TransactionByAccountEntity {
+
+ @PrimaryKeyColumn(name = "source_account_id", type = PrimaryKeyType.PARTITIONED)
+ public UUID sourceAccountId;
+
+ @PrimaryKeyColumn(name = "created_at", type = PrimaryKeyType.CLUSTERED, ordering = Ordering.DESCENDING)
+ public Long createdAt;
+
+ @PrimaryKeyColumn(name = "transaction_id", type = PrimaryKeyType.CLUSTERED)
+ public UUID transactionId;
+
+ @Column("destination_account_id")
+ public UUID destinationAccountId;
+
+ public TransactionStatus status;
+
+ public TransactionType type;
+
+ public float amount;
+}
\ No newline at end of file
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/mapper/TransactionByAccountMapper.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/mapper/TransactionByAccountMapper.java
new file mode 100644
index 0000000000..dce1581bfe
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/mapper/TransactionByAccountMapper.java
@@ -0,0 +1,31 @@
+package com.msucre.transaction.infrastructure.cassandra.mapper;
+
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.infrastructure.cassandra.entity.TransactionByAccountEntity;
+
+public class TransactionByAccountMapper {
+
+ public static TransactionByAccountEntity toEntity(final Transaction transaction) {
+ TransactionByAccountEntity transactionByAccountEntity = new TransactionByAccountEntity();
+ transactionByAccountEntity.transactionId = transaction.id;
+ transactionByAccountEntity.sourceAccountId = transaction.sourceAccountId;
+ transactionByAccountEntity.destinationAccountId = transaction.destinationAccountId;
+ transactionByAccountEntity.createdAt = transaction.createdAt;
+ transactionByAccountEntity.status = transaction.status;
+ transactionByAccountEntity.amount = transaction.amount;
+ transactionByAccountEntity.type = transaction.type;
+ return transactionByAccountEntity;
+ }
+
+ public static Transaction toTransaction(final TransactionByAccountEntity transactionByAccountEntity) {
+ Transaction transaction = new Transaction();
+ transaction.id = transactionByAccountEntity.transactionId;
+ transaction.sourceAccountId = transactionByAccountEntity.sourceAccountId;
+ transaction.destinationAccountId = transactionByAccountEntity.destinationAccountId;
+ transaction.createdAt = transactionByAccountEntity.createdAt;
+ transaction.status = transactionByAccountEntity.status;
+ transaction.amount = transactionByAccountEntity.amount;
+ transaction.type = transactionByAccountEntity.type;
+ return transaction;
+ }
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/repository/TransactionByAccountEntityRepository.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/repository/TransactionByAccountEntityRepository.java
new file mode 100644
index 0000000000..951e0489c5
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/repository/TransactionByAccountEntityRepository.java
@@ -0,0 +1,23 @@
+package com.msucre.transaction.infrastructure.cassandra.repository;
+
+import com.msucre.transaction.infrastructure.cassandra.entity.TransactionByAccountEntity;
+import org.springframework.data.cassandra.repository.Query;
+import org.springframework.data.cassandra.repository.ReactiveCassandraRepository;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Flux;
+
+import java.util.UUID;
+
+@Repository
+public interface TransactionByAccountEntityRepository extends ReactiveCassandraRepository {
+
+ @Query("""
+ SELECT
+ *
+ FROM
+ transaction_by_account
+ WHERE
+ source_account_id = ?0
+ """)
+ Flux findAllBySourceAccountId(final UUID sourceAccountId);
+}
\ No newline at end of file
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/repository/impl/TransactionByAccountRepositoryImpl.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/repository/impl/TransactionByAccountRepositoryImpl.java
new file mode 100644
index 0000000000..bdfdabdfce
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/cassandra/repository/impl/TransactionByAccountRepositoryImpl.java
@@ -0,0 +1,42 @@
+package com.msucre.transaction.infrastructure.cassandra.repository.impl;
+
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.domain.model.type.TransactionStatus;
+import com.msucre.transaction.domain.repository.TransactionByAccountRepository;
+import com.msucre.transaction.infrastructure.cassandra.mapper.TransactionByAccountMapper;
+import com.msucre.transaction.infrastructure.cassandra.repository.TransactionByAccountEntityRepository;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.Collections;
+import java.util.UUID;
+
+@Repository
+public class TransactionByAccountRepositoryImpl implements TransactionByAccountRepository {
+
+ private final TransactionByAccountEntityRepository transactionByAccountEntityRepository;
+
+ public TransactionByAccountRepositoryImpl(TransactionByAccountEntityRepository transactionByAccountEntityRepository) {
+ this.transactionByAccountEntityRepository = transactionByAccountEntityRepository;
+ }
+
+ @Override
+ public Mono save(Transaction transaction) {
+ return transactionByAccountEntityRepository.save(TransactionByAccountMapper.toEntity(transaction)).then();
+ }
+
+ @Override
+ public Flux findTransactionsByAccount(UUID accountId) {
+ return transactionByAccountEntityRepository.findAllBySourceAccountId(accountId)
+ .map(TransactionByAccountMapper::toTransaction);
+ }
+
+ @Override
+ public Mono updateStatus(Transaction transaction, TransactionStatus status) {
+ return Mono.fromCallable(() -> {
+ transaction.status = status;
+ return TransactionByAccountMapper.toEntity(transaction);
+ }).flatMap(transactionByAccountEntityRepository::save).then();
+ }
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/controller/TransactionController.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/controller/TransactionController.java
new file mode 100644
index 0000000000..5fd5191cd7
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/controller/TransactionController.java
@@ -0,0 +1,45 @@
+package com.msucre.transaction.infrastructure.controller;
+
+import com.msucre.transaction.application.usecase.transaction.CreateTransactionCase;
+import com.msucre.transaction.application.usecase.transaction.FindTransactionCase;
+import com.msucre.transaction.application.usecase.transaction.FindTransactionsByAccountCase;
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.domain.model.ui.CreateTransactionResponse;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@RestController
+@RequestMapping("/transaction")
+public class TransactionController {
+
+ private final CreateTransactionCase createTransactionCase;
+
+ private final FindTransactionCase findTransactionCase;
+
+ private final FindTransactionsByAccountCase findTransactionsByAccountCase;
+
+
+ public TransactionController(CreateTransactionCase createTransactionCase, FindTransactionCase findTransactionCase,
+ FindTransactionsByAccountCase findTransactionsByAccountCase) {
+ this.createTransactionCase = createTransactionCase;
+ this.findTransactionCase = findTransactionCase;
+ this.findTransactionsByAccountCase = findTransactionsByAccountCase;
+ }
+
+ @PostMapping
+ public Mono handleCreateTransaction(@RequestBody Transaction transaction) {
+ return createTransactionCase.execute(transaction)
+ .map(transactionId -> new CreateTransactionResponse("Transaction created successfully", transactionId));
+ }
+
+ @GetMapping("/{id}")
+ public Mono handleGetTransaction(@PathVariable("id") String id) {
+ return findTransactionCase.execute(id);
+ }
+
+ @GetMapping("/account/{accountId}")
+ public Flux handleGetTransactionsByAccount(@PathVariable("accountId") String accountId) {
+ return findTransactionsByAccountCase.execute(accountId);
+ }
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/kafka/configuration/KafkaConfig.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/kafka/configuration/KafkaConfig.java
new file mode 100644
index 0000000000..4eb117c0cd
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/kafka/configuration/KafkaConfig.java
@@ -0,0 +1,47 @@
+package com.msucre.transaction.infrastructure.kafka.configuration;
+
+import com.msucre.shared.model.dto.event.TransactionEventDto;
+import com.msucre.shared.model.dto.event.TransactionEventResponseDto;
+import com.msucre.shared.model.kafka.KafkaSharedConstants;
+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.boot.autoconfigure.kafka.KafkaProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.kafka.core.reactive.ReactiveKafkaConsumerTemplate;
+import org.springframework.kafka.core.reactive.ReactiveKafkaProducerTemplate;
+import org.springframework.kafka.support.serializer.JsonDeserializer;
+import org.springframework.kafka.support.serializer.JsonSerializer;
+import reactor.kafka.receiver.ReceiverOptions;
+import reactor.kafka.sender.SenderOptions;
+
+import java.util.Collections;
+import java.util.Map;
+
+@Configuration
+public class KafkaConfig {
+
+ @Bean
+ public ReactiveKafkaProducerTemplate reactiveKafkaProducerTemplate(
+ KafkaProperties properties) {
+ Map producerProperties = properties.buildProducerProperties();
+ producerProperties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
+ producerProperties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
+ return new ReactiveKafkaProducerTemplate<>(SenderOptions.create(producerProperties));
+ }
+
+ @Bean
+ public ReactiveKafkaConsumerTemplate reactiveKafkaConsumerTemplate(
+ KafkaProperties properties) {
+ Map consumerProperties = properties.buildProducerProperties();
+ consumerProperties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
+ consumerProperties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
+ consumerProperties.put(ConsumerConfig.GROUP_ID_CONFIG, KafkaSharedConstants.KAFKA_GROUP_ID);
+ consumerProperties.put(JsonDeserializer.TRUSTED_PACKAGES, KafkaSharedConstants.MODEL_TRUSTED_PACKAGE);
+ ReceiverOptions receiverOptions = ReceiverOptions.create(
+ consumerProperties).subscription(Collections.singletonList(KafkaSharedConstants.EVENT_RESPONSE_TOPIC));
+ return new ReactiveKafkaConsumerTemplate<>(receiverOptions);
+ }
+}
\ No newline at end of file
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/kafka/service/KafkaEventService.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/kafka/service/KafkaEventService.java
new file mode 100644
index 0000000000..842e9e4e27
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/kafka/service/KafkaEventService.java
@@ -0,0 +1,47 @@
+package com.msucre.transaction.infrastructure.kafka.service;
+
+
+import com.msucre.transaction.domain.service.EventService;
+import com.msucre.shared.model.dto.event.TransactionEventDto;
+import com.msucre.shared.model.dto.event.TransactionEventResponseDto;
+import com.msucre.shared.model.kafka.KafkaSharedConstants;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.kafka.core.reactive.ReactiveKafkaConsumerTemplate;
+import org.springframework.kafka.core.reactive.ReactiveKafkaProducerTemplate;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+@Service
+public class KafkaEventService implements EventService {
+
+ private static final Logger logger = LoggerFactory.getLogger(KafkaEventService.class);
+
+ private final ReactiveKafkaProducerTemplate kafkaProducerTemplate;
+
+ private final ReactiveKafkaConsumerTemplate kafkaConsumerTemplate;
+
+ public KafkaEventService(ReactiveKafkaProducerTemplate kafkaProducerTemplate,
+ ReactiveKafkaConsumerTemplate kafkaConsumerTemplate) {
+ this.kafkaProducerTemplate = kafkaProducerTemplate;
+ this.kafkaConsumerTemplate = kafkaConsumerTemplate;
+ }
+
+ @Override
+ public Mono sendTransactionEvent(final TransactionEventDto transactionEventDto) {
+ return kafkaProducerTemplate.send(KafkaSharedConstants.EVENT_TOPIC, transactionEventDto).doOnSuccess(
+ unused -> logger.debug("Sending event: [{} - {}]", transactionEventDto.transactionId(),
+ transactionEventDto.amount())).then();
+ }
+
+ @Override
+ public Flux consumeTransactionEventResponses() {
+ return kafkaConsumerTemplate.receiveAutoAck().map(ConsumerRecord::value).doOnNext(
+ transactionEventResponse -> logger.debug("Transaction validated: {} - {}",
+ transactionEventResponse.transactionId(),
+ transactionEventResponse.isValid()))
+ .onErrorContinue((throwable, o) -> logger.error("Error while consuming transaction event", throwable));
+ }
+}
\ No newline at end of file
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/entity/AccountEntity.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/entity/AccountEntity.java
new file mode 100644
index 0000000000..6c483ae418
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/entity/AccountEntity.java
@@ -0,0 +1,19 @@
+package com.msucre.transaction.infrastructure.postgresql.entity;
+
+
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.util.UUID;
+
+
+@Table(name = "account")
+public class AccountEntity {
+
+ @Id
+ public UUID id;
+
+ @Column
+ public String owner;
+}
\ No newline at end of file
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/entity/TransactionEntity.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/entity/TransactionEntity.java
new file mode 100644
index 0000000000..da283c4b2d
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/entity/TransactionEntity.java
@@ -0,0 +1,31 @@
+package com.msucre.transaction.infrastructure.postgresql.entity;
+
+import com.msucre.transaction.domain.model.type.TransactionStatus;
+import com.msucre.transaction.domain.model.type.TransactionType;
+import org.springframework.data.annotation.Id;
+import org.springframework.data.relational.core.mapping.Column;
+import org.springframework.data.relational.core.mapping.Table;
+
+import java.util.UUID;
+
+@Table(name = "transaction")
+public class TransactionEntity {
+
+ @Id
+ public UUID id;
+
+ @Column("source_account_id")
+ public UUID sourceAccountId;
+
+ @Column("created_at")
+ public Long createdAt;
+
+ @Column("destination_account_id")
+ public UUID destinationAccountId;
+
+ public TransactionStatus status;
+
+ public TransactionType type;
+
+ public float amount;
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/mapper/TransactionMapper.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/mapper/TransactionMapper.java
new file mode 100644
index 0000000000..f15b382b63
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/mapper/TransactionMapper.java
@@ -0,0 +1,31 @@
+package com.msucre.transaction.infrastructure.postgresql.mapper;
+
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.infrastructure.postgresql.entity.TransactionEntity;
+
+public class TransactionMapper {
+
+ public static TransactionEntity toEntity(Transaction transaction) {
+ TransactionEntity transactionEntity = new TransactionEntity();
+ transactionEntity.id = transaction.id;
+ transactionEntity.sourceAccountId = transaction.sourceAccountId;
+ transactionEntity.destinationAccountId = transaction.destinationAccountId;
+ transactionEntity.createdAt = transaction.createdAt;
+ transactionEntity.status = transaction.status;
+ transactionEntity.type = transaction.type;
+ transactionEntity.amount = transaction.amount;
+ return transactionEntity;
+ }
+
+ public static Transaction toTransaction(TransactionEntity transactionEntity) {
+ Transaction transaction = new Transaction();
+ transaction.id = transactionEntity.id;
+ transaction.sourceAccountId = transactionEntity.sourceAccountId;
+ transaction.destinationAccountId = transactionEntity.destinationAccountId;
+ transaction.createdAt = transactionEntity.createdAt;
+ transaction.status = transactionEntity.status;
+ transaction.type = transactionEntity.type;
+ transaction.amount = transactionEntity.amount;
+ return transaction;
+ }
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/repository/TransactionEntityRepository.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/repository/TransactionEntityRepository.java
new file mode 100644
index 0000000000..ec08178218
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/repository/TransactionEntityRepository.java
@@ -0,0 +1,12 @@
+package com.msucre.transaction.infrastructure.postgresql.repository;
+
+import com.msucre.transaction.infrastructure.postgresql.entity.TransactionEntity;
+import org.springframework.data.repository.reactive.ReactiveCrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.UUID;
+
+@Repository
+public interface TransactionEntityRepository extends ReactiveCrudRepository {
+
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/repository/impl/TransactionRepositoryImpl.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/repository/impl/TransactionRepositoryImpl.java
new file mode 100644
index 0000000000..b7d94d7460
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/postgresql/repository/impl/TransactionRepositoryImpl.java
@@ -0,0 +1,43 @@
+package com.msucre.transaction.infrastructure.postgresql.repository.impl;
+
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.domain.model.type.TransactionStatus;
+import com.msucre.transaction.domain.repository.TransactionRepository;
+import com.msucre.transaction.infrastructure.postgresql.mapper.TransactionMapper;
+import com.msucre.transaction.infrastructure.postgresql.repository.TransactionEntityRepository;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.util.UUID;
+
+@Component
+public class TransactionRepositoryImpl implements TransactionRepository {
+
+ private final TransactionEntityRepository transactionEntityRepository;
+
+ public TransactionRepositoryImpl(TransactionEntityRepository transactionEntityRepository) {
+ this.transactionEntityRepository = transactionEntityRepository;
+ }
+
+ @Override
+ public Mono save(Transaction transaction) {
+ return transactionEntityRepository.save(TransactionMapper.toEntity(transaction)).map(transactionEntity -> {
+ transaction.id = transactionEntity.id;
+ return transaction;
+ });
+ }
+
+ @Override
+ public Mono updateStatusById(String id, TransactionStatus status) {
+ return transactionEntityRepository.findById(UUID.fromString(id)).map(transactionEntity -> {
+ transactionEntity.status = status;
+ return transactionEntity;
+ }).flatMap(transactionEntityRepository::save)
+ .then();
+ }
+
+ @Override
+ public Mono findById(String id) {
+ return transactionEntityRepository.findById(UUID.fromString(id)).map(TransactionMapper::toTransaction);
+ }
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/redis/config/RedisConfig.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/redis/config/RedisConfig.java
new file mode 100644
index 0000000000..9948f17da0
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/redis/config/RedisConfig.java
@@ -0,0 +1,23 @@
+package com.msucre.transaction.infrastructure.redis.config;
+
+import com.msucre.transaction.domain.model.Transaction;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory;
+import org.springframework.data.redis.core.ReactiveRedisTemplate;
+import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
+import org.springframework.data.redis.serializer.RedisSerializationContext;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+@Configuration
+public class RedisConfig {
+
+ @Bean
+ ReactiveRedisTemplate redisOperations(ReactiveRedisConnectionFactory factory) {
+ Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Transaction.class);
+ RedisSerializationContext.RedisSerializationContextBuilder builder =
+ RedisSerializationContext.newSerializationContext(new StringRedisSerializer());
+ RedisSerializationContext context = builder.value(serializer).build();
+ return new ReactiveRedisTemplate<>(factory, context);
+ }
+}
diff --git a/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/redis/repository/RedisTransactionCacheRepository.java b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/redis/repository/RedisTransactionCacheRepository.java
new file mode 100644
index 0000000000..07cff7783c
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/java/com/msucre/transaction/infrastructure/redis/repository/RedisTransactionCacheRepository.java
@@ -0,0 +1,43 @@
+package com.msucre.transaction.infrastructure.redis.repository;
+
+import com.msucre.transaction.domain.model.Transaction;
+import com.msucre.transaction.domain.model.type.TransactionStatus;
+import com.msucre.transaction.domain.repository.TransactionCacheRepository;
+import org.springframework.data.redis.core.ReactiveRedisTemplate;
+import org.springframework.stereotype.Repository;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+@Repository
+public class RedisTransactionCacheRepository implements TransactionCacheRepository {
+
+ private static final long TTL_MINUTES = 5;
+
+ private final ReactiveRedisTemplate reactiveRedisTemplate;
+
+
+ public RedisTransactionCacheRepository(ReactiveRedisTemplate reactiveRedisTemplate) {
+ this.reactiveRedisTemplate = reactiveRedisTemplate;
+ }
+
+ @Override
+ public Mono save(Transaction transaction) {
+ return reactiveRedisTemplate.opsForValue()
+ .set(transaction.id.toString(), transaction, Duration.ofMinutes(TTL_MINUTES)).then();
+ }
+
+ @Override
+ public Mono updateStatusById(String id, TransactionStatus status) {
+ return reactiveRedisTemplate.opsForValue().get(id).map(transaction -> {
+ transaction.status = status;
+ return transaction;
+ })
+ .flatMap(updatedTransaction -> reactiveRedisTemplate.opsForValue().set(id, updatedTransaction)).then();
+ }
+
+ @Override
+ public Mono findById(String id) {
+ return reactiveRedisTemplate.opsForValue().get(id);
+ }
+}
diff --git a/YapeChallenge/TransactionManager/src/main/resources/application.properties b/YapeChallenge/TransactionManager/src/main/resources/application.properties
new file mode 100644
index 0000000000..c6825f69dd
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/main/resources/application.properties
@@ -0,0 +1,21 @@
+spring.application.name=TransactionManager
+server.port=8082
+
+spring.cassandra.contact-points=cassandra
+spring.cassandra.port=9042
+spring.cassandra.local-datacenter=dc1
+spring.cassandra.keyspace-name=yape_challenge
+
+spring.r2dbc.url=r2dbc:postgresql://postgres:5432/${POSTGRES_DB_NAME}
+spring.r2dbc.username=${POSTGRES_USER}
+spring.r2dbc.password=${POSTGRES_PASSWORD}
+
+spring.data.redis.host=redis
+spring.data.redis.port=6379
+spring.data.redis.timeout=2s
+
+spring.kafka.bootstrap-servers=kafka:9092
+
+# Set root logging level
+logging.level.com.msucre.transaction=DEBUG
+logging.level.org.apache.kafka.clients.producer=WARN
\ No newline at end of file
diff --git a/YapeChallenge/TransactionManager/src/test/java/com/msucre/transaction/TransactionManagerApplicationTests.java b/YapeChallenge/TransactionManager/src/test/java/com/msucre/transaction/TransactionManagerApplicationTests.java
new file mode 100644
index 0000000000..e94d961373
--- /dev/null
+++ b/YapeChallenge/TransactionManager/src/test/java/com/msucre/transaction/TransactionManagerApplicationTests.java
@@ -0,0 +1,9 @@
+package com.msucre.transaction;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class TransactionManagerApplicationTests {
+
+}
diff --git a/YapeChallenge/pom.xml b/YapeChallenge/pom.xml
new file mode 100644
index 0000000000..904d850200
--- /dev/null
+++ b/YapeChallenge/pom.xml
@@ -0,0 +1,79 @@
+
+
+ 4.0.0
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.9
+
+
+
+ com.msucre
+ yape-challenge
+ 1.0.0
+ pom
+ YapeChallenge
+ project for Spring Boot
+
+
+
+ AntiFraudManager
+ SharedModel
+ TransactionManager
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 21
+ 1.3.25
+ 1.10.2
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ 3.5.9
+ pom
+ import
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 0e8807f21c..f9d5087b1e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -1,25 +1,98 @@
-version: "3.7"
services:
postgres:
- image: postgres:14
+ image: postgres:18
+ container_name: postgres
ports:
- "5432:5432"
environment:
- - POSTGRES_USER=postgres
- - POSTGRES_PASSWORD=postgres
+ - POSTGRES_USER=${POSTGRES_USER}
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
+ - POSTGRES_DB=${POSTGRES_DB_NAME}
+ volumes:
+ - ./docker/postgres/init:/docker-entrypoint-initdb.d
+ networks:
+ - yape-network
zookeeper:
- image: confluentinc/cp-zookeeper:5.5.3
+ image: confluentinc/cp-zookeeper:7.7.7
+ container_name: zookeeper
environment:
ZOOKEEPER_CLIENT_PORT: 2181
+ networks:
+ - yape-network
kafka:
- image: confluentinc/cp-enterprise-kafka:5.5.3
+ image: confluentinc/cp-enterprise-kafka:7.7.7
+ container_name: kafka
depends_on: [zookeeper]
environment:
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
- KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
+ KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://kafka:9092
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
- KAFKA_BROKER_ID: 1
+ KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
- KAFKA_JMX_PORT: 9991
+ KAFKA_CREATE_TOPICS: "event:1:1,event-response:1:1"
ports:
- 9092:9092
+ - 29092:29092
+ networks:
+ - yape-network
+ cassandra:
+ image: cassandra:5.0
+ container_name: cassandra
+ ports:
+ - "9042:9042"
+ environment:
+ CASSANDRA_CLUSTER_NAME: "yape-challenge-cluster"
+ CASSANDRA_DC: dc1
+ CASSANDRA_ENDPOINT_SNITCH: GossipingPropertyFileSnitch
+ CASSANDRA_NUM_TOKENS: 1
+ CASSANDRA_RACK: rack1
+ healthcheck:
+ test: ["CMD-SHELL", "cqlsh localhost 9042 -e 'DESCRIBE KEYSPACES'"]
+ interval: 30s
+ timeout: 10s
+ retries: 10
+ networks:
+ - yape-network
+ cassandra-baseline:
+ image: cassandra:5.0
+ container_name: cassandra-baseline
+ depends_on:
+ cassandra:
+ condition: service_healthy
+ volumes:
+ - ./docker/cassandra/init/baseline.cql:/baseline.cql
+ command: /bin/bash -c "echo loading cassandra keyspace && cqlsh cassandra -f /baseline.cql && echo done"
+ networks:
+ - yape-network
+ redis:
+ image: redis:8.4
+ container_name: redis
+ ports:
+ - "6379:6379"
+ networks:
+ - yape-network
+ antifraud:
+ build:
+ dockerfile: ./Dockerfile.antifraud
+ container_name: antifraud
+ depends_on:
+ cassandra-baseline:
+ condition: service_completed_successfully
+ ports:
+ - "8081:8081"
+ networks:
+ - yape-network
+ transaction:
+ build:
+ dockerfile: ./Dockerfile.transaction
+ container_name: transaction
+ depends_on:
+ cassandra-baseline:
+ condition: service_completed_successfully
+ ports:
+ - "8082:8082"
+ networks:
+ - yape-network
+networks:
+ yape-network:
+ driver: bridge
\ No newline at end of file
diff --git a/docker/cassandra/init/baseline.cql b/docker/cassandra/init/baseline.cql
new file mode 100644
index 0000000000..47788fc7ff
--- /dev/null
+++ b/docker/cassandra/init/baseline.cql
@@ -0,0 +1,12 @@
+CREATE KEYSPACE IF NOT EXISTS yape_challenge WITH replication = {'class': 'SimpleStrategy', 'replication_factor': 1};
+
+CREATE TABLE IF NOT EXISTS yape_challenge.transaction_by_account (
+ source_account_id uuid,
+ created_at bigint,
+ transaction_id uuid,
+ destination_account_id uuid,
+ status text,
+ type text,
+ amount float,
+ PRIMARY KEY (source_account_id, created_at, transaction_id)
+) WITH CLUSTERING ORDER BY (created_at DESC, transaction_id ASC);
\ No newline at end of file
diff --git a/docker/postgres/init/baseline.sql b/docker/postgres/init/baseline.sql
new file mode 100644
index 0000000000..9177387ce7
--- /dev/null
+++ b/docker/postgres/init/baseline.sql
@@ -0,0 +1,9 @@
+CREATE TABLE IF NOT EXISTS transaction (
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
+ source_account_id uuid NOT NULL,
+ created_at bigint NOT NULL,
+ destination_account_id uuid NOT NULL,
+ status text NOT NULL,
+ type text NOT NULL,
+ amount float NOT NULL
+);
\ No newline at end of file