diff --git a/README.md b/README.md index 7f832ad..d249e7d 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,52 @@ -# Yape Code Challenge :rocket: +# SOLUCIÓN -Our code challenge will let you marvel us with your Jedi coding skills :smile:. +La solución que planteé fue entregar poco a poco valor al proyecto, de esta manera simulé que tenía pases a producción. +A través de uso de HU, Tasks, y gitflow para llevar a cabo todos los cambios. -Don't forget that the proper way to submit your work is to fork the repo and create a PR :wink: ... have fun !! +## RAMAS Y SU HU -- [Yape Code Challenge :rocket:](#yape-code-challenge-rocket) -- [Problem](#problem) -- [Tech Stack](#tech-stack) - - [Optional](#optional) -- [Send us your challenge](#send-us-your-challenge) +1. feature/basic-creation-transaction HU - Creación del servicio para guardar una transacción en la base de datos +2. chore/test-configuration FIX - The unit test was corrected and DevOps folder was created +3. feature/basic-retrieve-transaction HU - Creación del servicio para recuperar una transacción de la base de datos +4. feature/cache-transaction HU - Almacenar en cache data que se recupera de la base de datos +5. feature/event-transaction HU - Creación de componentes de mensajería y agregación en la capa service para enviarlo al microservicio de anti fraude para el análisis de las transacciones +7. feature/event-validate-transaction HU - Creación de componentes de mensajería y agregación en la capa service para evaluar las transacciones que llegan como evento y retornar el estado de la validación +8. chore/readme TASK - Creación de README +9. develop RELEASE 0.0.1 - Proyecto local +10. chore/docker-image HU - Contenedorización del microservicio de transacciones financieras +11. chore/docker-image HU - Contenedorización del microservicio de anti fraude +12. develop RELEASE 0.0.2 - Proyecto contenedorizado +13. chore/contract-first HU - Creación de contratos +14. feat/exception-handlers HU - Manejo de errores +15. chore/properties-prd-tested HU - Configuración de propiedades para PRD +16. develop RELEASE 0.0.3 - Propiedades PRD -# Problem +## REPOSITORIOS -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: +Trabajé dentro de mi cuenta en donde se podrá visualizar el árbol de ramas, los commits y los tags|releases -
    -
  1. pending
  2. -
  3. approved
  4. -
  5. rejected
  6. -
+## MS FINANCIAL TRANSACTION -Every transaction with a value greater than 1000 should be rejected. +https://github.com/CiprianoBryan/ms-financial-transaction/releases -```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)] -``` +## MS ANTIFRAUD -# Tech Stack +https://github.com/CiprianoBryan/ms-antifraud/releases -
    -
  1. Java. You can use any framework you want
  2. -
  3. Any database
  4. -
  5. Kafka
  6. -
+## USO DEL PROYECTO -We do provide a `Dockerfile` to help you get started with a dev environment. +- Puede utilizar el tag 0.0.1 si desea el proyecto totalmente local +- Puede utilizar el tag 0.0.2 si desea el proyecto dockerizado +- Puede utilizar el tag 0.0.3 si lleva el proyecto a Kubernetes y utiliza los valores de producción necesarios para cumplir con la alta transaccionalidad -You must have two resources: +## PUNTOS DE MEJORA -1. Resource to create a transaction that must containt: +- En un primer planteamiento tenía pensado utilizar CQRS para optimizar más el tiempo de respuesta, utilizando una base de datos principal y otra réplica, por el tiempo nos quedamos con tener únicamente una base de datos +- Agregar los tests unitarios, el cuál por tiempo tampoco se pudo dar +- Utilizar Quarkus para aprovechar aún más Kubernetes y tener el proyecto más optimizado -```json -{ - "accountExternalIdDebit": "Guid", - "accountExternalIdCredit": "Guid", - "tranferTypeId": 1, - "value": 120 -} -``` +## TENER EN CUENTA -2. Resource to retrieve a transaction - -```json -{ - "transactionExternalId": "Guid", - "transactionType": { - "name": "" - }, - "transactionStatus": { - "name": "" - }, - "value": 120, - "createdAt": "Date" -} -``` - -## Optional - -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? - -You can use Graphql; - -# Send us your challenge - -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. \ No newline at end of file +- Usar Reactividad podría generar conflictos por lo que no se recomendaría +- Tener dashboard para monitorear la aplicación (En alguna de las herramientas de observabilidad Dynatrace, New Relic, entre otros) +- Se está agregando los archivos de cada repositorio en este fork junto al .git para que lo pueda descargar y visualizar todo lo trabajado diff --git a/ms-antifraud/.gitignore b/ms-antifraud/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/ms-antifraud/.gitignore @@ -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/ diff --git a/ms-antifraud/Dockerfile b/ms-antifraud/Dockerfile new file mode 100644 index 0000000..4c16faf --- /dev/null +++ b/ms-antifraud/Dockerfile @@ -0,0 +1,20 @@ +FROM maven:3.9.12-eclipse-temurin-25-alpine AS builder + +WORKDIR /app + +COPY pom.xml . +RUN mvn -B -q -e -DskipTests dependency:go-offline + +COPY src ./src + +RUN mvn clean package -DskipTests + +FROM eclipse-temurin:25-jdk + +WORKDIR /app + +COPY --from=builder /app/target/*.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/ms-antifraud/README.md b/ms-antifraud/README.md new file mode 100644 index 0000000..c37b4f6 --- /dev/null +++ b/ms-antifraud/README.md @@ -0,0 +1,98 @@ +# MICROSERVICIO DE ANTI FRAUDE + +## Descripción + +Este microservicio se encarga de revisar y validar las diferentes operaciones del banco (ejm: Transacciones financieras) + +## Funcionamiento + +Recupera los eventos de un tópico a través de un consumer para luego en la capa service evaluarlo usando las reglas de negocio, una vez evaluado se publica en otro tópico del cual el microservicio del proyecto owner de la operación lo obtendrá con el nuevo estado. + +## Diagrama de componentes + +Módulo Transaction + +```mermaid +flowchart LR + +%% ================================ +%% EVENT STREAMING LAYER +%% ================================ + +subgraph Event_Streaming_Kafka_Cluster + TC[TOPIC
transaction-created] + TV[TOPIC
transaction-validated] +end + +%% ================================ +%% MICROSERVICE LAYER +%% ================================ + +subgraph Kubernetes_Cluster + MS[MICROSERVICE
ms-antifraud
module: Transaction] +end + +%% ================================ +%% FLOW +%% ================================ + +MS -->|Consume| TC +MS -->|Publish Result| TV +``` + +## Levantar el proyecto localmente + +1. Levantar kafka, esto ya está incluido en los pasos para levantar el proyecto ms-financial-transaction indicados del README +2. Levantar el proyecto localmente (Active profiles: local), tener instalado Java 25 + +## Levantar el proyecto usando docker (v0.0.2) + +El objetivo de esta versión del proyecto es para que los usuarios técnicos que deseen utilizar el microservicio puedan levantar todos los recursos utilizando docker y probar el funcionamiento del microservicio. + +IMPORTANTE: Ubícate en la ruta de la raíz del proyecto + +1. Ejecutar los siguientes comandos para levantar cada servicio que son kafka y el microservicio de anti fraude. Además de crear los tópicos. + + ### Opción 1 + + - Si el servicio ms-financial-transaction no levantó correctamente es probable que la causa sea porque un servicio anterior falló, debe de reintentar ejecutar el comando en unos 15 segundos aprox. + + ```bash + $ docker compose up -d + ``` + + ### Opción 2 + + - Tener en cuenta al levantar el servicio ms-financial-transaction deben estar levantados todos los anteriores servicios. Esperar entre cada servicio unos 15 segundos aprox. + + ```bash + $ docker compose up -d kafka + $ docker compose up -d ms-antifraud + ``` + + ### Crear Tópicos + + ```bash + $ docker exec -it kafka_server bash + $ kafka-topics --bootstrap-server localhost:9092 --create --topic transaction-created --partitions 1 --replication-factor 1 + $ kafka-topics --bootstrap-server localhost:9092 --create --topic transaction-validated --partitions 1 --replication-factor 1 + ``` + +## Valores a si deseas utilizarlo o probar localmente + +- Active profiles: local +- Version 0.0.1: levantar local +- Version 0.0.2: levantar el contenedor + +## Valores a usar para la alta transaccionalidad (Utilizar Active Profiles: dev para apuntar a estos valores) + +TPS: 8000 + +Se realizan pruebas de performance utilizando JMeter, de donde obtenemos una configuración recomendable. + +### En Kubernetes + +- KAFKA + - Número de hilos para consumidores kafka: 3 + - Total de consumidores: 6 pods x 3 = 18 consumers + - Cantidad de particiones: 18 (cada consumer podrá consumir de una partición sin bloquearse) diff --git a/ms-antifraud/docker-compose.yml b/ms-antifraud/docker-compose.yml new file mode 100644 index 0000000..36e01ab --- /dev/null +++ b/ms-antifraud/docker-compose.yml @@ -0,0 +1,38 @@ +name: yape-resources +services: + zookeeper: + image: confluentinc/cp-zookeeper:5.5.3 + container_name: zookeeper_server + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + kafka: + image: confluentinc/cp-enterprise-kafka:5.5.3 + container_name: kafka_server + depends_on: [ zookeeper ] + environment: + KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" + 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_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9991 + ports: + - "9092:9092" + ms-antifraud: + build: . + image: ciprianobryan/ms-antifraud:0.0.1 + container_name: ms-antifraud + depends_on: + - kafka + ports: + - "8081:8080" + environment: + TRANSACTION_MAX_AMOUNT_TO_BE_APROVED: 1000 + + KAFKA_BOOTSTRAP_SERVERS: kafka:9092 + KAFKA_CONSUMER_GROUP_ID: antifraud-group + KAFKA_TOPIC_TRANSACTION_CREATED: transaction-created + KAFKA_TOPIC_TRANSACTION_VALIDATED: transaction-validated + +volumes: + postgres_data: diff --git a/ms-antifraud/pom.xml b/ms-antifraud/pom.xml new file mode 100644 index 0000000..dffa7b2 --- /dev/null +++ b/ms-antifraud/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + com.yape + ms-antifraud + 0.0.3 + ms-antifraud + ms-antifraud + + + 25 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-kafka + + + + org.projectlombok + lombok + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/ms-antifraud/src/main/java/com/yape/antifraud/AntifraudApplication.java b/ms-antifraud/src/main/java/com/yape/antifraud/AntifraudApplication.java new file mode 100644 index 0000000..9ab3a50 --- /dev/null +++ b/ms-antifraud/src/main/java/com/yape/antifraud/AntifraudApplication.java @@ -0,0 +1,13 @@ +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); + } + +} diff --git a/ms-antifraud/src/main/java/com/yape/antifraud/kafka/TransactionCreatedConsumer.java b/ms-antifraud/src/main/java/com/yape/antifraud/kafka/TransactionCreatedConsumer.java new file mode 100644 index 0000000..dff61de --- /dev/null +++ b/ms-antifraud/src/main/java/com/yape/antifraud/kafka/TransactionCreatedConsumer.java @@ -0,0 +1,23 @@ +package com.yape.antifraud.kafka; + +import com.yape.antifraud.kafka.event.TransactionCreatedEvent; +import com.yape.antifraud.service.AntifraudService; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +public class TransactionCreatedConsumer { + private final AntifraudService antifraudService; + + public TransactionCreatedConsumer(AntifraudService antifraudService) { + this.antifraudService = antifraudService; + } + + @KafkaListener( + topics = "${app.kafka.topics.transaction-created.name}", + groupId = "${spring.kafka.consumer.group-id}" + ) + public void consume(TransactionCreatedEvent event) { + antifraudService.validateTransaction(event); + } +} diff --git a/ms-antifraud/src/main/java/com/yape/antifraud/kafka/TransactionValidatedProducer.java b/ms-antifraud/src/main/java/com/yape/antifraud/kafka/TransactionValidatedProducer.java new file mode 100644 index 0000000..ba0a614 --- /dev/null +++ b/ms-antifraud/src/main/java/com/yape/antifraud/kafka/TransactionValidatedProducer.java @@ -0,0 +1,22 @@ +package com.yape.antifraud.kafka; + +import com.yape.antifraud.kafka.event.TransactionValidatedEvent; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Component +public class TransactionValidatedProducer { + private final KafkaTemplate kafkaTemplate; + + public TransactionValidatedProducer(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Value("${app.kafka.topics.transaction-validated.name}") + private String topic; + + public void produce(TransactionValidatedEvent event) { + kafkaTemplate.send(topic, event); + } +} diff --git a/ms-antifraud/src/main/java/com/yape/antifraud/kafka/event/TransactionCreatedEvent.java b/ms-antifraud/src/main/java/com/yape/antifraud/kafka/event/TransactionCreatedEvent.java new file mode 100644 index 0000000..0601a41 --- /dev/null +++ b/ms-antifraud/src/main/java/com/yape/antifraud/kafka/event/TransactionCreatedEvent.java @@ -0,0 +1,13 @@ +package com.yape.antifraud.kafka.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.math.BigDecimal; + +@Getter +@AllArgsConstructor +public class TransactionCreatedEvent { + private Long transactionId; + private BigDecimal value; +} diff --git a/ms-antifraud/src/main/java/com/yape/antifraud/kafka/event/TransactionValidatedEvent.java b/ms-antifraud/src/main/java/com/yape/antifraud/kafka/event/TransactionValidatedEvent.java new file mode 100644 index 0000000..9f949e6 --- /dev/null +++ b/ms-antifraud/src/main/java/com/yape/antifraud/kafka/event/TransactionValidatedEvent.java @@ -0,0 +1,11 @@ +package com.yape.antifraud.kafka.event; + +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class TransactionValidatedEvent { + private Long transactionId; + private Integer statusId; +} diff --git a/ms-antifraud/src/main/java/com/yape/antifraud/model/TransactionStatus.java b/ms-antifraud/src/main/java/com/yape/antifraud/model/TransactionStatus.java new file mode 100644 index 0000000..8c01b81 --- /dev/null +++ b/ms-antifraud/src/main/java/com/yape/antifraud/model/TransactionStatus.java @@ -0,0 +1,12 @@ +package com.yape.antifraud.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum TransactionStatus { + PENDING, + APPROVED, + REJECTED; +} diff --git a/ms-antifraud/src/main/java/com/yape/antifraud/service/AntifraudService.java b/ms-antifraud/src/main/java/com/yape/antifraud/service/AntifraudService.java new file mode 100644 index 0000000..e9c856c --- /dev/null +++ b/ms-antifraud/src/main/java/com/yape/antifraud/service/AntifraudService.java @@ -0,0 +1,7 @@ +package com.yape.antifraud.service; + +import com.yape.antifraud.kafka.event.TransactionCreatedEvent; + +public interface AntifraudService { + void validateTransaction(TransactionCreatedEvent event); +} diff --git a/ms-antifraud/src/main/java/com/yape/antifraud/service/impl/AntifraudServiceImpl.java b/ms-antifraud/src/main/java/com/yape/antifraud/service/impl/AntifraudServiceImpl.java new file mode 100644 index 0000000..87328ad --- /dev/null +++ b/ms-antifraud/src/main/java/com/yape/antifraud/service/impl/AntifraudServiceImpl.java @@ -0,0 +1,35 @@ +package com.yape.antifraud.service.impl; + +import com.yape.antifraud.kafka.TransactionValidatedProducer; +import com.yape.antifraud.kafka.event.TransactionCreatedEvent; +import com.yape.antifraud.kafka.event.TransactionValidatedEvent; +import com.yape.antifraud.model.TransactionStatus; +import com.yape.antifraud.service.AntifraudService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +public class AntifraudServiceImpl implements AntifraudService { + private final TransactionValidatedProducer transactionValidatedProducer; + + public AntifraudServiceImpl(TransactionValidatedProducer transactionValidatedProducer) { + this.transactionValidatedProducer = transactionValidatedProducer; + } + + @Value("${app.transaction.max-amount}") + private BigDecimal valueMaxLimit; + + @Override + public void validateTransaction(TransactionCreatedEvent event) { + TransactionStatus transactionStatus = event.getValue().compareTo(valueMaxLimit) > 0 + ? TransactionStatus.REJECTED + : TransactionStatus.APPROVED; + + transactionValidatedProducer.produce(TransactionValidatedEvent.builder() + .transactionId(event.getTransactionId()) + .statusId(transactionStatus.ordinal()) + .build()); + } +} diff --git a/ms-antifraud/src/main/resources/application-dev.yaml b/ms-antifraud/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..e445b98 --- /dev/null +++ b/ms-antifraud/src/main/resources/application-dev.yaml @@ -0,0 +1,9 @@ +server.port: 8081 + +TRANSACTION_MAX_AMOUNT_TO_BE_APROVED: 1000 + +KAFKA_BOOTSTRAP_SERVERS: localhost:9092 +KAFKA_CONSUMER_GROUP_ID: antifraud-group +KAFKA_TOPIC_TRANSACTION_CREATED: transaction-created +KAFKA_TOPIC_TRANSACTION_VALIDATED: transaction-validated +KAFKA_LISTENER_CONCURRENCY: 3 diff --git a/ms-antifraud/src/main/resources/application-local.yaml b/ms-antifraud/src/main/resources/application-local.yaml new file mode 100644 index 0000000..35fbfc8 --- /dev/null +++ b/ms-antifraud/src/main/resources/application-local.yaml @@ -0,0 +1,9 @@ +server.port: 8081 + +TRANSACTION_MAX_AMOUNT_TO_BE_APROVED: 1000 + +KAFKA_BOOTSTRAP_SERVERS: localhost:9092 +KAFKA_CONSUMER_GROUP_ID: antifraud-group +KAFKA_TOPIC_TRANSACTION_CREATED: transaction-created +KAFKA_TOPIC_TRANSACTION_VALIDATED: transaction-validated +KAFKA_LISTENER_CONCURRENCY: 2 diff --git a/ms-antifraud/src/main/resources/application.yaml b/ms-antifraud/src/main/resources/application.yaml new file mode 100644 index 0000000..bdb0766 --- /dev/null +++ b/ms-antifraud/src/main/resources/application.yaml @@ -0,0 +1,32 @@ +spring: + application: + name: ms-antifraud + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JacksonJsonSerializer + acks: all + properties: + enable.idempotence: true + consumer: + group-id: ${KAFKA_CONSUMER_GROUP_ID} + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JacksonJsonDeserializer + properties: + spring.json.trusted.packages: "com.yape.antifraud.kafka.event" + spring.json.value.default.type: "com.yape.antifraud.kafka.event.TransactionCreatedEvent" + spring.json.use.type.headers: false + listener: + missing-topics-fatal: false + concurrency: ${KAFKA_LISTENER_CONCURRENCY} + +app: + transaction: + max-amount: ${TRANSACTION_MAX_AMOUNT_TO_BE_APROVED} + kafka: + topics: + transaction-created: + name: ${KAFKA_TOPIC_TRANSACTION_CREATED} + transaction-validated: + name: ${KAFKA_TOPIC_TRANSACTION_VALIDATED} diff --git a/ms-antifraud/src/test/java/com/yape/antifraud/AntifraudApplicationTests.java b/ms-antifraud/src/test/java/com/yape/antifraud/AntifraudApplicationTests.java new file mode 100644 index 0000000..ca3859b --- /dev/null +++ b/ms-antifraud/src/test/java/com/yape/antifraud/AntifraudApplicationTests.java @@ -0,0 +1,13 @@ +package com.yape.antifraud; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class AntifraudApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/ms-antifraud/swagger/async-api.yaml b/ms-antifraud/swagger/async-api.yaml new file mode 100644 index 0000000..6a41328 --- /dev/null +++ b/ms-antifraud/swagger/async-api.yaml @@ -0,0 +1,92 @@ +asyncapi: 3.0.0 +info: + title: Anti Fraud Events API + version: 1.0.0 + description: | + API encargada de validar las operaciones aplicando reglas de negocio antifraude. + En esta versión 0.0.2 contamos con el módulo de Transactions para recibir los eventos con la información de una transacción financiera para evaluarla aplicando + las reglas de negocio antifraude en operaciones de transacciones para finalmente enviar el evento con el estado actualizado para que el equipo de transacciones lo pueda recuperar. + +servers: + kafka: + host: localhost:9092 + protocol: kafka-secure + description: Local broker + +channels: + transaction-created: + address: transaction-created + messages: + TransactionCreatedEvent: + $ref: '#/components/messages/TransactionCreatedEvent' + description: Tópico donde el Microservicio de Antifraude recibe las nuevas transacciones para ser validadas. + + transaction-validated: + address: transaction-validated + messages: + TransactionValidatedEvent: + $ref: '#/components/messages/TransactionValidatedEvent' + description: Tópico donde el Microservicio de Antifraude publica el resultado de la validación. + +operations: + publishCreatedTransaction: + action: receive + channel: + $ref: '#/channels/transaction-created' + summary: Publica una transacción recién creada para validación. + + receiveValidatedTransaction: + action: send + channel: + $ref: '#/channels/transaction-validated' + summary: Escucha el resultado de la validación para actualizar el estado en la BD. + +components: + messages: + TransactionCreatedEvent: + name: TransactionCreatedEvent + title: Evento de Transacción Creada + summary: Notifica que una transacción ha sido registrada y requiere validación. + payload: + $ref: '#/components/schemas/TransactionCreatedPayload' + + TransactionValidatedEvent: + name: TransactionValidatedEvent + title: Evento de Transacción Validada + summary: Resultado del proceso de validación antifraude. + payload: + $ref: '#/components/schemas/TransactionValidatedPayload' + + schemas: + TransactionCreatedPayload: + type: object + required: + - transactionExternalId + - value + properties: + transactionExternalId: + type: integer + description: Identificador único global de la transacción. + example: 2 + value: + type: number + format: decimal + minimum: 0 + example: 120.50 + description: Monto de la transacción para evaluar el riesgo. + + TransactionValidatedPayload: + type: object + required: + - transactionExternalId + - status + properties: + transactionExternalId: + type: integer + description: Identificador de la transacción validada. + example: 2 + status: + type: integer + enum: [0, 1] + example: 1 + description: "Resultado final tras aplicar la regla de negocio (Rechazado por monto mayor a 1000, 1: APPROVED, 2: REJECTED)." \ No newline at end of file diff --git a/ms-financial-transaction/.gitignore b/ms-financial-transaction/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/ms-financial-transaction/.gitignore @@ -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/ diff --git a/ms-financial-transaction/Dockerfile b/ms-financial-transaction/Dockerfile new file mode 100644 index 0000000..4c16faf --- /dev/null +++ b/ms-financial-transaction/Dockerfile @@ -0,0 +1,20 @@ +FROM maven:3.9.12-eclipse-temurin-25-alpine AS builder + +WORKDIR /app + +COPY pom.xml . +RUN mvn -B -q -e -DskipTests dependency:go-offline + +COPY src ./src + +RUN mvn clean package -DskipTests + +FROM eclipse-temurin:25-jdk + +WORKDIR /app + +COPY --from=builder /app/target/*.jar app.jar + +EXPOSE 8080 + +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/ms-financial-transaction/README.md b/ms-financial-transaction/README.md new file mode 100644 index 0000000..3e205ac --- /dev/null +++ b/ms-financial-transaction/README.md @@ -0,0 +1,173 @@ +# MICROSERVICIO DE TRANSACCIONES FINANCIERAS + +## Descripción + +Este microservicio se encarga de guardar una nueva transacción en la base de datos y enviarlo a que sea evaluado por el microservicio de anti-fraude. Además también brinda el servicio de recuperar de una transacción guardada en la base de datos. + +## Funcionamiento + +Recibe las nuevas transacciones, después las guarda en la base de datos con el estado PENDING, y luego se publica la información de la transacción para que sea evaluada en el tópico, finalmente a través de un consumer obtendrá el evento de un tópico con la información de la transacción evaluada, y guarda el nuevo estado en la base de datos. +También brinda las transacciones que fueron almacenados en la base de datos, en caso que se consulte más de una vez la misma información, se retornará la respuesta almacenada en caché Redis, esta información dura 1 minuto en la caché. + +## Diagrama de componentes + +### Guardar Transacciones financieras + +```mermaid +flowchart LR + +%% ================================ +%% MICROSERVICE LAYER +%% ================================ + +subgraph Kubernetes_Cluster + MS[MICROSERVICE
ms-financial-transaction] +end + +%% ================================ +%% EVENT STREAMING LAYER +%% ================================ + +subgraph Event_Streaming_Kafka_Cluster + TC[TOPIC
transaction-created] + TV[TOPIC
transaction-validated] +end + +%% ================================ +%% DATABASE LAYER +%% ================================ + +subgraph Database + DB[DATABASE
yape_db] +end + +%% ================================ +%% FLOW +%% ================================ + +MS -->|Store| DB +MS -->|Publish| TC +MS -->|Consume| TV +MS -->|Update status| DB +``` + +### Recuperar una transacción financiera + +```mermaid +flowchart LR + +%% ================================ +%% MICROSERVICE LAYER +%% ================================ + +subgraph Kubernetes_Cluster + MS[MICROSERVICE
ms-financial-transaction] +end + +%% ================================ +%% DATABASE LAYER +%% ================================ + +subgraph Database + DB[DATABASE
yape_db] +end + +%% ================================ +%% REDIS LAYER +%% ================================ + +subgraph Redis + RD[REDIS
data_yape_cache] +end + +%% ================================ +%% FLOW +%% ================================ + +MS -->|Retrieve| DB +MS -->|Store| RD +``` + +## Levantar el proyecto localmente (v0.0.1) + +El objetivo de esta versión del proyecto es para que los desarrolladores tengan un proyecto listo y funcional para que puedan añadir nuevas funcionalidades y probarlo. + +IMPORTANTE: Ubícate en la ruta de la raíz del proyecto + +1. Ejecutar los siguientes comandos para levantar la base de datos postgres y kafka. + + ```bash + $ docker compose -f .\devops\docker-compose.yml up -d + $ docker exec -it kafka_server bash + $ kafka-topics --bootstrap-server localhost:9092 --create --topic transaction-created --partitions 1 --replication-factor 1 + $ kafka-topics --bootstrap-server localhost:9092 --create --topic transaction-validated --partitions 1 --replication-factor 1 + ``` + +2. Levantar localmente Redis +3. Levantar el proyecto localmente (Active profiles: local), tener instalado Java 25 + +## Levantar el proyecto usando docker (v0.0.2) + +El objetivo de esta versión del proyecto es para que los usuarios técnicos que deseen utilizar el microservicio puedan levantar todos los recursos utilizando docker y probar el funcionamiento del microservicio. + +IMPORTANTE: Ubícate en la ruta de la raíz del proyecto + +1. Ejecutar los siguientes comandos para levantar cada servicio que son la base de datos postgres, kafka, redis, y finalmente el microservicio de transacciones financieras. Además de crear los tópicos. + + ### Opción 1 + + - Si el servicio ms-financial-transaction no levantó correctamente es probable que la causa sea porque un servicio anterior falló, debe de reintentar ejecutar el comando en unos 15 segundos aprox. + + ```bash + $ docker compose up -d + ``` + + ### Opción 2 + + - Tener en cuenta al levantar el servicio ms-financial-transaction deben estar levantados todos los anteriores servicios. Esperar entre cada servicio unos 15 segundos aprox. + + ```bash + $ docker compose up -d postgres + $ docker compose up -d zookeeper + $ docker compose up -d redis + $ docker compose up -d kafka + $ docker compose up -d ms-financial-transaction + ``` + + ### Crear Tópicos + + ```bash + $ docker exec -it kafka_server bash + $ kafka-topics --bootstrap-server localhost:9092 --create --topic transaction-created --partitions 1 --replication-factor 1 + $ kafka-topics --bootstrap-server localhost:9092 --create --topic transaction-validated --partitions 1 --replication-factor 1 + ``` + +## Valores a si deseas utilizarlo o probar localmente + +- Active profiles: local +- Version 0.0.1: levantar local +- Version 0.0.2: levantar el contenedor + +## Valores a usar para la alta transaccionalidad (Utilizar Active Profiles: dev para apuntar a estos valores) + +TPS: 8000 + +Se realizan pruebas de performance utilizando JMeter, de donde obtenemos una configuración recomendable. + +### En Kubernetes + + - BASE DE DATOS + - Mínima cantidad de pods (minReplicas): 4 + - Máxima cantidad de pods (maxReplicas): 6 + - Máxima cantidad de pools de la DB por pod: 50 (Total de conexiones: 6 pods x 50 conexiones = 300 conexiones) Tener en cuenta que la base de datos adquirida tenga una cantidad de conexiones por encima de los 350 (SHOW max_connections) PostgreSQL con más de 16 cores + - REDIS + - TTL Redis: 180 segundos + - Data REDIS: 4GB RAM separado para REDIS + - activa: TPS x TTL = 8000 x 180 = 1,440,000 keys activas + - Memoria REDIS = Claves x Tamaño (2KB aprox) = 2,8880,000 KB = 2.8GB + - Agregando Overhead (30%): 3.6 a 4GB + - Estrategia de optimización de memoria en Redis: maxmemory-policy = allkeys-lru + - KAFKA + - Número de hilos para consumidores kafka: 3 + - Total de consumidores: 6 pods x 3 = 18 consumers + - Cantidad de particiones: 18 (cada consumer podrá consumir de una partición sin bloquearse) diff --git a/docker-compose.yml b/ms-financial-transaction/docker-compose-local.yml similarity index 72% rename from docker-compose.yml rename to ms-financial-transaction/docker-compose-local.yml index 6e9a9c5..6b5ac03 100644 --- a/docker-compose.yml +++ b/ms-financial-transaction/docker-compose-local.yml @@ -1,18 +1,21 @@ -version: "3.7" services: postgres: image: postgres:14 + container_name: postgres_server_local ports: - "5432:5432" environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=yape_db zookeeper: image: confluentinc/cp-zookeeper:5.5.3 + container_name: zookeeper_server_local environment: ZOOKEEPER_CLIENT_PORT: 2181 kafka: image: confluentinc/cp-enterprise-kafka:5.5.3 + container_name: kafka_server_local depends_on: [zookeeper] environment: KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" @@ -22,4 +25,9 @@ services: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 KAFKA_JMX_PORT: 9991 ports: - - 9092:9092 \ No newline at end of file + - 9092:9092 + redis: + image: redis:7 + container_name: redis_server_local + ports: + - "6379:6379" diff --git a/ms-financial-transaction/docker-compose.yml b/ms-financial-transaction/docker-compose.yml new file mode 100644 index 0000000..232b179 --- /dev/null +++ b/ms-financial-transaction/docker-compose.yml @@ -0,0 +1,69 @@ +name: yape-resources +services: + postgres: + image: postgres:14 + container_name: postgres_server + ports: + - "5432:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=yape_db + volumes: + - postgres_data:/var/lib/postgresql/data + zookeeper: + image: confluentinc/cp-zookeeper:5.5.3 + container_name: zookeeper_server + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + kafka: + image: confluentinc/cp-enterprise-kafka:5.5.3 + container_name: kafka_server + depends_on: [ zookeeper ] + environment: + KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" + 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_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9991 + ports: + - "9092:9092" + redis: + image: redis:7 + container_name: redis_server + ports: + - "6379:6379" + ms-financial-transaction: + build: . + image: ciprianobryan/ms-financial-transaction:0.0.2 + container_name: ms-financial-transaction + depends_on: + - postgres + - kafka + - redis + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/yape_db + SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver + SPRING_DATASOURCE_USERNAME: postgres + SPRING_DATASOURCE_PASSWORD: postgres + SPRING_JPA_SHOW_SQL: false + SPRING_SQL_INIT_MODE: always + + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_EXPIRATION_SECONDS: 60 + + KAFKA_BOOTSTRAP_SERVERS: kafka:9092 + KAFKA_CONSUMER_GROUP_ID: financial-transaction-group + KAFKA_TOPIC_TRANSACTION_CREATED: transaction-created + KAFKA_TOPIC_TRANSACTION_CREATED_PARTITIONS: 1 + KAFKA_TOPIC_TRANSACTION_CREATED_REPLICAS: 1 + KAFKA_TOPIC_TRANSACTION_VALIDATED: transaction-validated + KAFKA_TOPIC_TRANSACTION_VALIDATED_PARTITIONS: 1 + KAFKA_TOPIC_TRANSACTION_VALIDATED_REPLICAS: 1 + +volumes: + postgres_data: diff --git a/ms-financial-transaction/pom.xml b/ms-financial-transaction/pom.xml new file mode 100644 index 0000000..4ce7391 --- /dev/null +++ b/ms-financial-transaction/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.0.3 + + + com.yape + ms-financial-transaction + 0.0.3 + ms-financial-transaction + ms-financial-transaction + + + 25 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-kafka + + + + org.springframework.boot + spring-boot-starter-validation + + + org.projectlombok + lombok + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-data-jpa-test + test + + + com.h2database + h2 + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/MsFinancialTransactionApplication.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/MsFinancialTransactionApplication.java new file mode 100644 index 0000000..2a35507 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/MsFinancialTransactionApplication.java @@ -0,0 +1,13 @@ +package com.yape.financialtransaction; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MsFinancialTransactionApplication { + + public static void main(String[] args) { + SpringApplication.run(MsFinancialTransactionApplication.class, args); + } + +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/configuration/RedisConfiguration.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/configuration/RedisConfiguration.java new file mode 100644 index 0000000..0e07058 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/configuration/RedisConfiguration.java @@ -0,0 +1,22 @@ +package com.yape.financialtransaction.configuration; + +import com.yape.financialtransaction.controller.dto.RetrieveTransactionResponseDto; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.JacksonJsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfiguration { + @Bean + public RedisTemplate redisTemplate( + RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new JacksonJsonRedisSerializer<>(RetrieveTransactionResponseDto.class)); + return redisTemplate; + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/TransactionController.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/TransactionController.java new file mode 100644 index 0000000..3b67f14 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/TransactionController.java @@ -0,0 +1,38 @@ +package com.yape.financialtransaction.controller; + +import com.yape.financialtransaction.controller.dto.CreateTransactionRequestDto; +import com.yape.financialtransaction.controller.dto.CreateTransactionResponseDto; +import com.yape.financialtransaction.controller.dto.RetrieveTransactionResponseDto; +import com.yape.financialtransaction.service.TransactionService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.UUID; + +@RequestMapping("/transaction") +@RestController +public class TransactionController { + private final TransactionService transactionService; + + public TransactionController(TransactionService transactionService) { + this.transactionService = transactionService; + } + + @PostMapping + public ResponseEntity createTransaction( + @Valid @RequestBody CreateTransactionRequestDto request) { + return ResponseEntity.ok(transactionService.createTransaction(request)); + } + + @GetMapping("/{transactionExternalId}") + public ResponseEntity retrieveTransaction( + @PathVariable UUID transactionExternalId) { + return ResponseEntity.ok(transactionService.retrieveTransaction(transactionExternalId)); + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/CreateTransactionRequestDto.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/CreateTransactionRequestDto.java new file mode 100644 index 0000000..7e7a963 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/CreateTransactionRequestDto.java @@ -0,0 +1,21 @@ +package com.yape.financialtransaction.controller.dto; + +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; +import org.hibernate.validator.constraints.Range; + +import java.math.BigDecimal; +import java.util.UUID; + +public record CreateTransactionRequestDto( + @NotNull + UUID accountExternalIdDebit, + @NotNull + UUID accountExternalIdCredit, + @NotNull + @Range(min = 0, max = 1, message = "El valor solo puede ser 0:DEPOSIT o 1:WITHDRAWAL") + Integer transactionTypeId, + @NotNull + @Positive(message = "El valor de la transacción debe ser mayor a cero") + BigDecimal value) { +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/CreateTransactionResponseDto.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/CreateTransactionResponseDto.java new file mode 100644 index 0000000..61ba11b --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/CreateTransactionResponseDto.java @@ -0,0 +1,15 @@ +package com.yape.financialtransaction.controller.dto; + +import lombok.Builder; + +import java.math.BigDecimal; +import java.util.UUID; + +@Builder +public record CreateTransactionResponseDto( + UUID transactionExternalId, + TransactionTypeDto transactionType, + TransactionStatusDto transactionStatus, + BigDecimal value, + String createdAt) { +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/ErrorResponseDto.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/ErrorResponseDto.java new file mode 100644 index 0000000..8122f7d --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/ErrorResponseDto.java @@ -0,0 +1,8 @@ +package com.yape.financialtransaction.controller.dto; + +import lombok.Builder; + +@Builder +public record ErrorResponseDto( + String name) { +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/RetrieveTransactionResponseDto.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/RetrieveTransactionResponseDto.java new file mode 100644 index 0000000..65d9a73 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/RetrieveTransactionResponseDto.java @@ -0,0 +1,15 @@ +package com.yape.financialtransaction.controller.dto; + +import lombok.Builder; + +import java.math.BigDecimal; +import java.util.UUID; + +@Builder +public record RetrieveTransactionResponseDto( + UUID transactionExternalId, + TransactionTypeDto transactionType, + TransactionStatusDto transactionStatus, + BigDecimal value, + String createdAt) { +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/TransactionStatusDto.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/TransactionStatusDto.java new file mode 100644 index 0000000..66e4d62 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/TransactionStatusDto.java @@ -0,0 +1,8 @@ +package com.yape.financialtransaction.controller.dto; + +import lombok.Builder; + +@Builder +public record TransactionStatusDto( + String name) { +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/TransactionTypeDto.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/TransactionTypeDto.java new file mode 100644 index 0000000..fe60cbb --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/controller/dto/TransactionTypeDto.java @@ -0,0 +1,8 @@ +package com.yape.financialtransaction.controller.dto; + +import lombok.Builder; + +@Builder +public record TransactionTypeDto( + String name) { +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/AppServerException.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/AppServerException.java new file mode 100644 index 0000000..0dd77c6 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/AppServerException.java @@ -0,0 +1,7 @@ +package com.yape.financialtransaction.exception; + +public class AppServerException extends RuntimeException { + public AppServerException(Throwable cause) { + super(cause); + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/GlobalExceptionHandler.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..b6fe56f --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/GlobalExceptionHandler.java @@ -0,0 +1,43 @@ +package com.yape.financialtransaction.exception; + +import com.yape.financialtransaction.controller.dto.ErrorResponseDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +@Slf4j +@RestControllerAdvice +public class GlobalExceptionHandler { + private final MessageError messageError; + + public GlobalExceptionHandler(MessageError messageError) { + this.messageError = messageError; + } + + @ExceptionHandler({ + MethodArgumentNotValidException.class, + HttpMessageNotReadableException.class, + MethodArgumentTypeMismatchException.class}) + public ResponseEntity handleValidationExceptions(Exception exception) { + log.error(exception.getMessage(), exception); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponseDto(messageError.getRequestInvalid())); + } + + @ExceptionHandler(TransactionNotFoundException.class) + public ResponseEntity handleTransactionNotFoundException(TransactionNotFoundException exception) { + return ResponseEntity.status(HttpStatus.NOT_FOUND) + .body(new ErrorResponseDto(messageError.getTransactionNotFound())); + } + + @ExceptionHandler(AppServerException.class) + public ResponseEntity handleAppServerException(AppServerException exception) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(new ErrorResponseDto(messageError.getInternalServerError())); + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/MessageError.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/MessageError.java new file mode 100644 index 0000000..2a8acca --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/MessageError.java @@ -0,0 +1,17 @@ +package com.yape.financialtransaction.exception; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Getter +@Setter +@Configuration +@ConfigurationProperties(prefix = "app.errors") +public class MessageError { + private String requestInvalid; + private String transactionIdInvalid; + private String transactionNotFound; + private String internalServerError; +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/TransactionNotFoundException.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/TransactionNotFoundException.java new file mode 100644 index 0000000..f76c196 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/exception/TransactionNotFoundException.java @@ -0,0 +1,7 @@ +package com.yape.financialtransaction.exception; + +public class TransactionNotFoundException extends RuntimeException { + public TransactionNotFoundException() { + super(); + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/TransactionCreatedProducer.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/TransactionCreatedProducer.java new file mode 100644 index 0000000..e9dafad --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/TransactionCreatedProducer.java @@ -0,0 +1,22 @@ +package com.yape.financialtransaction.kafka; + +import com.yape.financialtransaction.kafka.event.TransactionCreatedEvent; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Component +public class TransactionCreatedProducer { + private final KafkaTemplate kafkaTemplate; + + public TransactionCreatedProducer(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Value("${app.kafka.topics.transaction-created.name}") + private String topic; + + public void produce(TransactionCreatedEvent event) { + kafkaTemplate.send(topic, event); + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/TransactionValidatedConsumer.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/TransactionValidatedConsumer.java new file mode 100644 index 0000000..90667bc --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/TransactionValidatedConsumer.java @@ -0,0 +1,22 @@ +package com.yape.financialtransaction.kafka; + +import com.yape.financialtransaction.kafka.event.TransactionValidatedEvent; +import com.yape.financialtransaction.service.TransactionService; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +@Component +public class TransactionValidatedConsumer { + private final TransactionService transactionService; + + public TransactionValidatedConsumer(TransactionService transactionService) { + this.transactionService = transactionService; + } + + @KafkaListener( + topics = "${app.kafka.topics.transaction-validated.name}", + groupId = "${spring.kafka.consumer.group-id}") + public void consume(TransactionValidatedEvent event) { + transactionService.updateTransactionStatus(event.getTransactionId(), event.getStatusId()); + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/event/TransactionCreatedEvent.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/event/TransactionCreatedEvent.java new file mode 100644 index 0000000..b5dd90f --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/event/TransactionCreatedEvent.java @@ -0,0 +1,13 @@ +package com.yape.financialtransaction.kafka.event; + +import lombok.Builder; +import lombok.Getter; + +import java.math.BigDecimal; + +@Getter +@Builder +public class TransactionCreatedEvent { + private Long transactionId; + private BigDecimal value; +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/event/TransactionValidatedEvent.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/event/TransactionValidatedEvent.java new file mode 100644 index 0000000..cc5ff5d --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/kafka/event/TransactionValidatedEvent.java @@ -0,0 +1,11 @@ +package com.yape.financialtransaction.kafka.event; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TransactionValidatedEvent { + private Long transactionId; + private Integer statusId; +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/model/TransactionStatus.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/model/TransactionStatus.java new file mode 100644 index 0000000..6f7d89f --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/model/TransactionStatus.java @@ -0,0 +1,16 @@ +package com.yape.financialtransaction.model; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum TransactionStatus { + PENDING, + APPROVED, + REJECTED; + + public static String nameFromId(Integer id) { + return TransactionStatus.values()[id].name(); + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/model/TransactionType.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/model/TransactionType.java new file mode 100644 index 0000000..ffb0b95 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/model/TransactionType.java @@ -0,0 +1,13 @@ +package com.yape.financialtransaction.model; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum TransactionType { + DEPOSIT, + WITHDRAWAL; + + public static String nameFromId(Integer id) { + return TransactionType.values()[id].name(); + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/repository/TransactionRepository.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/repository/TransactionRepository.java new file mode 100644 index 0000000..d866a9f --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/repository/TransactionRepository.java @@ -0,0 +1,18 @@ +package com.yape.financialtransaction.repository; + +import com.yape.financialtransaction.repository.entity.Transaction; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.Optional; +import java.util.UUID; + +@Repository +public interface TransactionRepository extends JpaRepository { + Optional findByTransactionExternalId(UUID transactionExternalId); + @Modifying + @Query("UPDATE Transaction t SET t.statusId = :statusId WHERE t.id = :transactionId") + void updateStatusIdById(Long transactionId, Integer statusId); +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/repository/entity/Transaction.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/repository/entity/Transaction.java new file mode 100644 index 0000000..8c55517 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/repository/entity/Transaction.java @@ -0,0 +1,55 @@ +package com.yape.financialtransaction.repository.entity; + +import com.yape.financialtransaction.model.TransactionStatus; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Index; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.hibernate.annotations.CreationTimestamp; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table(name = "financial_transaction", indexes = { + @Index(name = "idx_transaction_external_id", columnList = "transactionExternalId") +}) +@Entity +public class Transaction { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private UUID transactionExternalId; + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private Integer transactionTypeId; + @Column(precision = 19, scale = 2) + private BigDecimal value; + private Integer statusId; + @CreationTimestamp + private LocalDateTime createdAt; + + @PrePersist + private void onCreate() { + if (transactionExternalId == null) { + transactionExternalId = UUID.randomUUID(); + } + if (statusId == null) { + statusId = TransactionStatus.PENDING.ordinal(); + } + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/TransactionCacheService.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/TransactionCacheService.java new file mode 100644 index 0000000..9019d8c --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/TransactionCacheService.java @@ -0,0 +1,11 @@ +package com.yape.financialtransaction.service; + +import com.yape.financialtransaction.controller.dto.RetrieveTransactionResponseDto; + +import java.util.Optional; +import java.util.UUID; + +public interface TransactionCacheService { + Optional getFromCache(UUID key); + void saveToCache(UUID key, RetrieveTransactionResponseDto value); +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/TransactionService.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/TransactionService.java new file mode 100644 index 0000000..5f6a447 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/TransactionService.java @@ -0,0 +1,13 @@ +package com.yape.financialtransaction.service; + +import com.yape.financialtransaction.controller.dto.CreateTransactionRequestDto; +import com.yape.financialtransaction.controller.dto.CreateTransactionResponseDto; +import com.yape.financialtransaction.controller.dto.RetrieveTransactionResponseDto; + +import java.util.UUID; + +public interface TransactionService { + CreateTransactionResponseDto createTransaction(CreateTransactionRequestDto createTransactionRequestDto); + RetrieveTransactionResponseDto retrieveTransaction(UUID transactionExternalId); + void updateTransactionStatus(Long transactionId, Integer status); +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/impl/TransactionCacheServiceImpl.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/impl/TransactionCacheServiceImpl.java new file mode 100644 index 0000000..c81c769 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/impl/TransactionCacheServiceImpl.java @@ -0,0 +1,42 @@ +package com.yape.financialtransaction.service.impl; + +import com.yape.financialtransaction.controller.dto.RetrieveTransactionResponseDto; +import com.yape.financialtransaction.exception.AppServerException; +import com.yape.financialtransaction.service.TransactionCacheService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.Duration; +import java.util.Optional; +import java.util.UUID; + +@Service +public class TransactionCacheServiceImpl implements TransactionCacheService { + private final RedisTemplate redisTemplate; + + public TransactionCacheServiceImpl(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Value("${app.redis.cache.ttl}") + private Integer cacheExpirationSeconds; + + @Override + public Optional getFromCache(UUID key) { + try { + return Optional.ofNullable(redisTemplate.opsForValue().get(key.toString())); + } catch (Exception e) { + throw new AppServerException(e); + } + } + + @Override + public void saveToCache(UUID key, RetrieveTransactionResponseDto value) { + try { + redisTemplate.opsForValue().set(key.toString(), value, Duration.ofSeconds(cacheExpirationSeconds)); + } catch (Exception e) { + throw new AppServerException(e); + } + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/impl/TransactionServiceImpl.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/impl/TransactionServiceImpl.java new file mode 100644 index 0000000..6f003e7 --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/service/impl/TransactionServiceImpl.java @@ -0,0 +1,85 @@ +package com.yape.financialtransaction.service.impl; + +import com.yape.financialtransaction.controller.dto.CreateTransactionRequestDto; +import com.yape.financialtransaction.controller.dto.CreateTransactionResponseDto; +import com.yape.financialtransaction.controller.dto.RetrieveTransactionResponseDto; +import com.yape.financialtransaction.exception.AppServerException; +import com.yape.financialtransaction.exception.TransactionNotFoundException; +import com.yape.financialtransaction.kafka.TransactionCreatedProducer; +import com.yape.financialtransaction.kafka.event.TransactionCreatedEvent; +import com.yape.financialtransaction.repository.TransactionRepository; +import com.yape.financialtransaction.repository.entity.Transaction; +import com.yape.financialtransaction.service.TransactionCacheService; +import com.yape.financialtransaction.service.TransactionService; +import com.yape.financialtransaction.util.TransactionMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; +import java.util.UUID; + +@Slf4j +@Service +public class TransactionServiceImpl implements TransactionService { + private final TransactionRepository transactionRepository; + private final TransactionCacheService transactionCacheService; + private final TransactionCreatedProducer transactionCreatedProducer; + + public TransactionServiceImpl(TransactionRepository transactionRepository, + TransactionCacheService transactionCacheService, + TransactionCreatedProducer transactionCreatedProducer) { + this.transactionRepository = transactionRepository; + this.transactionCacheService = transactionCacheService; + this.transactionCreatedProducer = transactionCreatedProducer; + } + + @Override + public CreateTransactionResponseDto createTransaction(CreateTransactionRequestDto createTransactionRequestDto) { + try { + Transaction transactionEntity = TransactionMapper.toEntity(createTransactionRequestDto); + Transaction transactionCreated = transactionRepository.save(transactionEntity); + TransactionCreatedEvent transactionCreatedEvent = + TransactionMapper.toTransactionCreatedEvent(transactionCreated); + transactionCreatedProducer.produce(transactionCreatedEvent); + return TransactionMapper.toCreateTransactionResponseDto(transactionCreated); + } catch (Exception e) { + throw new AppServerException(e); + } + } + + @Override + public RetrieveTransactionResponseDto retrieveTransaction(UUID transactionExternalId) { + Optional transactionResponseDtoCache = + transactionCacheService.getFromCache(transactionExternalId); + if (transactionResponseDtoCache.isPresent()) { + return transactionResponseDtoCache.get(); + } + + Optional transactionRegister; + try { + transactionRegister = transactionRepository.findByTransactionExternalId(transactionExternalId); + } catch (Exception e) { + throw new AppServerException(e); + } + + if (transactionRegister.isEmpty()) { + log.error("Transaction with external ID {} not found", transactionExternalId); + throw new TransactionNotFoundException(); + } + RetrieveTransactionResponseDto response = + TransactionMapper.toRetrieveTransactionResponseDto(transactionRegister.get()); + transactionCacheService.saveToCache(transactionExternalId, response); + return response; + } + + @Transactional + @Override + public void updateTransactionStatus(Long transactionId, Integer statusId) { + try { + transactionRepository.updateStatusIdById(transactionId, statusId); + } catch (Exception e) { + throw new AppServerException(e); + } + } +} diff --git a/ms-financial-transaction/src/main/java/com/yape/financialtransaction/util/TransactionMapper.java b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/util/TransactionMapper.java new file mode 100644 index 0000000..1df1c4b --- /dev/null +++ b/ms-financial-transaction/src/main/java/com/yape/financialtransaction/util/TransactionMapper.java @@ -0,0 +1,57 @@ +package com.yape.financialtransaction.util; + +import com.yape.financialtransaction.controller.dto.CreateTransactionRequestDto; +import com.yape.financialtransaction.controller.dto.CreateTransactionResponseDto; +import com.yape.financialtransaction.controller.dto.RetrieveTransactionResponseDto; +import com.yape.financialtransaction.controller.dto.TransactionStatusDto; +import com.yape.financialtransaction.controller.dto.TransactionTypeDto; +import com.yape.financialtransaction.kafka.event.TransactionCreatedEvent; +import com.yape.financialtransaction.model.TransactionStatus; +import com.yape.financialtransaction.model.TransactionType; +import com.yape.financialtransaction.repository.entity.Transaction; + +public interface TransactionMapper { + static Transaction toEntity(CreateTransactionRequestDto requestDto) { + return Transaction.builder() + .accountExternalIdDebit(requestDto.accountExternalIdDebit()) + .accountExternalIdCredit(requestDto.accountExternalIdCredit()) + .transactionTypeId(requestDto.transactionTypeId()) + .value(requestDto.value()) + .build(); + } + + static CreateTransactionResponseDto toCreateTransactionResponseDto(Transaction transaction) { + return CreateTransactionResponseDto.builder() + .transactionExternalId(transaction.getTransactionExternalId()) + .transactionType(TransactionTypeDto.builder() + .name(TransactionType.nameFromId(transaction.getTransactionTypeId())) + .build()) + .transactionStatus(TransactionStatusDto.builder() + .name(TransactionStatus.nameFromId(transaction.getStatusId())) + .build()) + .value(transaction.getValue()) + .createdAt(transaction.getCreatedAt().toString()) + .build(); + } + + static RetrieveTransactionResponseDto toRetrieveTransactionResponseDto(Transaction transaction) { + return RetrieveTransactionResponseDto.builder() + .transactionExternalId(transaction.getTransactionExternalId()) + .transactionType(TransactionTypeDto.builder() + .name(TransactionType.nameFromId(transaction.getTransactionTypeId())) + .build()) + .transactionStatus(TransactionStatusDto.builder() + .name(TransactionStatus.nameFromId(transaction.getStatusId())) + .build()) + .value(transaction.getValue()) + .createdAt(transaction.getCreatedAt().toString()) + .build(); + } + + static TransactionCreatedEvent toTransactionCreatedEvent(Transaction transaction) { + return TransactionCreatedEvent.builder() + .transactionId(transaction.getId()) + .value(transaction.getValue()) + .build(); + } +} diff --git a/ms-financial-transaction/src/main/resources/application-dev.yaml b/ms-financial-transaction/src/main/resources/application-dev.yaml new file mode 100644 index 0000000..9925838 --- /dev/null +++ b/ms-financial-transaction/src/main/resources/application-dev.yaml @@ -0,0 +1,36 @@ +server.port: 8080 + +SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/yape_db +SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver +SPRING_DATASOURCE_USERNAME: postgres +SPRING_DATASOURCE_PASSWORD: postgres +SPRING_DATASOURCE_MAX_POOLS: 40 +SPRING_DATASOURCE_MINIMUM_IDLE: 15 +SPRING_DATASOURCE_CONNECTION_TIMEOUT: 20000 +SPRING_DATASOURCE_IDLE_TIMEOUT: 300000 +SPRING_DATASOURCE_MAX_LIFETIME: 600000 +SPRING_DATASOURCE_VALIDATION_TIMEOUT: 5000 +SPRING_JPA_SHOW_SQL: false +SPRING_SQL_INIT_MODE: always + +REDIS_HOST: localhost +REDIS_PORT: 6379 +REDIS_MAX_POOLS: 100 +REDIS_MAX_IDLE: 50 +REDIS_MIN_IDLE: 10 +REDIS_MAX_WAIT: 3000 +REDIS_EXPIRATION_SECONDS: 180 + +KAFKA_BOOTSTRAP_SERVERS: localhost:9092 +KAFKA_CONSUMER_GROUP_ID: financial-transaction-group +KAFKA_TOPIC_TRANSACTION_CREATED: transaction-created +KAFKA_TOPIC_TRANSACTION_CREATED_PARTITIONS: 1 +KAFKA_TOPIC_TRANSACTION_CREATED_REPLICAS: 1 +KAFKA_TOPIC_TRANSACTION_VALIDATED: transaction-validated +KAFKA_TOPIC_TRANSACTION_VALIDATED_PARTITIONS: 1 +KAFKA_TOPIC_TRANSACTION_VALIDATED_REPLICAS: 1 +KAFKA_LISTENER_CONCURRENCY: 3 + +MESSAGE_ERROR_REQUEST_INVALID: "Campos inválidos" +MESSAGE_ERROR_TRANSACTION_NOT_FOUND: "Transacción Externa no encontrada" +MESSAGE_ERROR_INTERNAL_SERVER_ERROR: "Error interno del servidor" diff --git a/ms-financial-transaction/src/main/resources/application-local.yaml b/ms-financial-transaction/src/main/resources/application-local.yaml new file mode 100644 index 0000000..e16d032 --- /dev/null +++ b/ms-financial-transaction/src/main/resources/application-local.yaml @@ -0,0 +1,36 @@ +server.port: 8080 + +SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/yape_db +SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver +SPRING_DATASOURCE_USERNAME: postgres +SPRING_DATASOURCE_PASSWORD: postgres +SPRING_DATASOURCE_MAX_POOLS: 10 +SPRING_DATASOURCE_MINIMUM_IDLE: 15 +SPRING_DATASOURCE_CONNECTION_TIMEOUT: 20000 +SPRING_DATASOURCE_IDLE_TIMEOUT: 300000 +SPRING_DATASOURCE_MAX_LIFETIME: 600000 +SPRING_DATASOURCE_VALIDATION_TIMEOUT: 5000 +SPRING_JPA_SHOW_SQL: false +SPRING_SQL_INIT_MODE: always + +REDIS_HOST: localhost +REDIS_PORT: 6379 +REDIS_MAX_POOLS: 10 +REDIS_MAX_IDLE: 50 +REDIS_MIN_IDLE: 10 +REDIS_MAX_WAIT: 3000 +REDIS_EXPIRATION_SECONDS: 60 + +KAFKA_BOOTSTRAP_SERVERS: localhost:9092 +KAFKA_CONSUMER_GROUP_ID: financial-transaction-group +KAFKA_TOPIC_TRANSACTION_CREATED: transaction-created +KAFKA_TOPIC_TRANSACTION_CREATED_PARTITIONS: 1 +KAFKA_TOPIC_TRANSACTION_CREATED_REPLICAS: 1 +KAFKA_TOPIC_TRANSACTION_VALIDATED: transaction-validated +KAFKA_TOPIC_TRANSACTION_VALIDATED_PARTITIONS: 1 +KAFKA_TOPIC_TRANSACTION_VALIDATED_REPLICAS: 1 +KAFKA_LISTENER_CONCURRENCY: 2 + +MESSAGE_ERROR_REQUEST_INVALID: "Campos inválidos" +MESSAGE_ERROR_TRANSACTION_NOT_FOUND: "Transacción Externa no encontrada" +MESSAGE_ERROR_INTERNAL_SERVER_ERROR: "Error interno del servidor" diff --git a/ms-financial-transaction/src/main/resources/application.yaml b/ms-financial-transaction/src/main/resources/application.yaml new file mode 100644 index 0000000..545d2d5 --- /dev/null +++ b/ms-financial-transaction/src/main/resources/application.yaml @@ -0,0 +1,74 @@ +spring: + application: + name: ms-financial-transaction + + datasource: + url: ${SPRING_DATASOURCE_URL} + driverClassName: ${SPRING_DATASOURCE_DRIVER_CLASS_NAME} + username: ${SPRING_DATASOURCE_USERNAME} + password: ${SPRING_DATASOURCE_PASSWORD} + hikari: + maximum-pool-size: ${SPRING_DATASOURCE_MAX_POOLS} + minimum-idle: ${SPRING_DATASOURCE_MINIMUM_IDLE} + connection-timeout: ${SPRING_DATASOURCE_CONNECTION_TIMEOUT} + idle-timeout: ${SPRING_DATASOURCE_IDLE_TIMEOUT} + max-lifetime: ${SPRING_DATASOURCE_MAX_LIFETIME} + validation-timeout: ${SPRING_DATASOURCE_VALIDATION_TIMEOUT} + jpa: + show-sql: ${SPRING_JPA_SHOW_SQL} + open-in-view: false + hibernate: + ddl-auto: none + sql: + init: + mode: ${SPRING_SQL_INIT_MODE} + schema-locations: classpath:sql/schema.sql + data: + redis: + host: ${REDIS_HOST} + port: ${REDIS_PORT} + timeout: ${REDIS_TIMEOUT} + lettuce: + pool: + max-active: ${REDIS_MAX_POOLS} + max-idle: ${REDIS_MAX_IDLE} + min-idle: ${REDIS_MIN_IDLE} + max-wait: ${REDIS_MAX_WAIT} + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JacksonJsonSerializer + acks: all + properties: + enable.idempotence: true + consumer: + group-id: ${KAFKA_CONSUMER_GROUP_ID} + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JacksonJsonDeserializer + properties: + spring.json.trusted.packages: "com.yape.financialtransaction.kafka.event" + spring.json.value.default.type: "com.yape.financialtransaction.kafka.event.TransactionValidatedEvent" + spring.json.use.type.headers: false + listener: + missing-topics-fatal: false + concurrency: ${KAFKA_LISTENER_CONCURRENCY} + +app: + redis: + cache: + ttl: ${REDIS_EXPIRATION_SECONDS} + kafka: + topics: + transaction-created: + name: ${KAFKA_TOPIC_TRANSACTION_CREATED} + partitions: ${KAFKA_TOPIC_TRANSACTION_CREATED_PARTITIONS} + replicas: ${KAFKA_TOPIC_TRANSACTION_CREATED_REPLICAS} + transaction-validated: + name: ${KAFKA_TOPIC_TRANSACTION_VALIDATED} + partitions: ${KAFKA_TOPIC_TRANSACTION_VALIDATED_PARTITIONS} + replicas: ${KAFKA_TOPIC_TRANSACTION_VALIDATED_REPLICAS} + errors: + request-invalid: ${MESSAGE_ERROR_REQUEST_INVALID} + transaction-not-found: ${MESSAGE_ERROR_TRANSACTION_NOT_FOUND} + internal-server-error: ${MESSAGE_ERROR_INTERNAL_SERVER_ERROR} diff --git a/ms-financial-transaction/src/main/resources/sql/schema.sql b/ms-financial-transaction/src/main/resources/sql/schema.sql new file mode 100644 index 0000000..5482b05 --- /dev/null +++ b/ms-financial-transaction/src/main/resources/sql/schema.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS financial_transaction ( + id BIGSERIAL PRIMARY KEY, + transaction_external_id UUID NOT NULL UNIQUE, + account_external_id_debit UUID NOT NULL, + account_external_id_credit UUID NOT NULL, + transaction_type_id INT NOT NULL, + value DECIMAL(19, 2) NOT NULL, + status_id INT NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_transaction_external_id ON financial_transaction(transaction_external_id); diff --git a/ms-financial-transaction/src/test/java/com/yape/financialtransaction/MsFinancialTransactionApplicationTests.java b/ms-financial-transaction/src/test/java/com/yape/financialtransaction/MsFinancialTransactionApplicationTests.java new file mode 100644 index 0000000..ea1e949 --- /dev/null +++ b/ms-financial-transaction/src/test/java/com/yape/financialtransaction/MsFinancialTransactionApplicationTests.java @@ -0,0 +1,13 @@ +package com.yape.financialtransaction; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class MsFinancialTransactionApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/ms-financial-transaction/src/test/resources/application.yml b/ms-financial-transaction/src/test/resources/application.yml new file mode 100644 index 0000000..1cba12d --- /dev/null +++ b/ms-financial-transaction/src/test/resources/application.yml @@ -0,0 +1,19 @@ +SPRING_DATASOURCE_URL: jdbc:h2:mem:testdb +SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.h2.Driver +SPRING_DATASOURCE_USERNAME: sa +SPRING_DATASOURCE_PASSWORD: +SPRING_JPA_SHOW_SQL: false +SPRING_SQL_INIT_MODE: never + +REDIS_HOST: localhost +REDIS_PORT: 6380 +REDIS_EXPIRATION_SECONDS: 60 + +KAFKA_BOOTSTRAP_SERVERS: localhost:9092 +KAFKA_CONSUMER_GROUP_ID: consumer-group-test +KAFKA_TOPIC_TRANSACTION_CREATED: transaction-created-topic-test +KAFKA_TOPIC_TRANSACTION_CREATED_PARTITIONS: 1 +KAFKA_TOPIC_TRANSACTION_CREATED_REPLICAS: 1 +KAFKA_TOPIC_TRANSACTION_VALIDATED: transaction-validated-topic-test +KAFKA_TOPIC_TRANSACTION_VALIDATED_PARTITIONS: 1 +KAFKA_TOPIC_TRANSACTION_VALIDATED_REPLICAS: 1 diff --git a/ms-financial-transaction/swagger/async-api.yaml b/ms-financial-transaction/swagger/async-api.yaml new file mode 100644 index 0000000..60d5d0e --- /dev/null +++ b/ms-financial-transaction/swagger/async-api.yaml @@ -0,0 +1,91 @@ +asyncapi: 3.0.0 +info: + title: Financial Transaction Events API + version: 1.0.0 + description: | + API de eventos para el flujo de validación de transacciones financieras y antifraude. + Incluye la creación de transacciones y la actualización de estados tras validación. + +servers: + kafka: + host: localhost:9092 + protocol: kafka-secure + description: Local broker + +channels: + transaction-created: + address: transaction-created + messages: + TransactionCreatedEvent: + $ref: '#/components/messages/TransactionCreatedEvent' + description: Tópico donde el Microservicio de Transacciones publica las nuevas transacciones para ser validadas. + + transaction-validated: + address: transaction-validated + messages: + TransactionValidatedEvent: + $ref: '#/components/messages/TransactionValidatedEvent' + description: Tópico donde el Microservicio de Transacciones recibe el resultado de la validación. + +operations: + publishCreatedTransaction: + action: send + channel: + $ref: '#/channels/transaction-created' + summary: Publica una transacción recién creada para validación. + + receiveValidatedTransaction: + action: receive + channel: + $ref: '#/channels/transaction-validated' + summary: Escucha el resultado de la validación para actualizar el estado en la BD. + +components: + messages: + TransactionCreatedEvent: + name: TransactionCreatedEvent + title: Evento de Transacción Creada + summary: Notifica que una transacción ha sido registrada y requiere validación. + payload: + $ref: '#/components/schemas/TransactionCreatedPayload' + + TransactionValidatedEvent: + name: TransactionValidatedEvent + title: Evento de Transacción Validada + summary: Resultado del proceso de validación antifraude. + payload: + $ref: '#/components/schemas/TransactionValidatedPayload' + + schemas: + TransactionCreatedPayload: + type: object + required: + - transactionExternalId + - value + properties: + transactionExternalId: + type: integer + description: Identificador único global de la transacción. + example: 2 + value: + type: number + format: decimal + minimum: 0 + example: 120.50 + description: Monto de la transacción para evaluar el riesgo. + + TransactionValidatedPayload: + type: object + required: + - transactionExternalId + - status + properties: + transactionExternalId: + type: integer + description: Identificador de la transacción validada. + example: 2 + status: + type: integer + enum: [0, 1] + example: 1 + description: "Resultado final tras aplicar la regla de negocio (Rechazado por monto mayor a 1000, 1: APPROVED, 2: REJECTED)." \ No newline at end of file diff --git a/ms-financial-transaction/swagger/rest-api.yaml b/ms-financial-transaction/swagger/rest-api.yaml new file mode 100644 index 0000000..e89bc59 --- /dev/null +++ b/ms-financial-transaction/swagger/rest-api.yaml @@ -0,0 +1,284 @@ +openapi: 3.0.0 +info: + title: MS Financial Transaction + version: 1.0.0 + description: API para la creación y consulta de transacciones financieras entre cuentas. + +servers: + - url: http://localhost:8080 + description: Servidor Local + +paths: + /transaction: + post: + summary: Crear una nueva transacción + operationId: createTransaction + tags: + - Transaction + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTransactionRequestDto' + examples: + CreateTransactionRequestDto200: + $ref: '#/components/examples/CreateTransactionRequestDto200' + CreateTransactionRequestDto400: + $ref: '#/components/examples/CreateTransactionRequestDto400' + responses: + '200': + description: Transacción creada exitosamente + content: + application/json: + schema: + $ref: '#/components/schemas/CreateTransactionResponseDto' + examples: + CreateTransactionResponseDto200: + $ref: '#/components/examples/CreateTransactionResponseDto200' + '400': + description: Datos de entrada inválidos + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + examples: + CreateTransactionResponseDto400: + $ref: '#/components/examples/CreateTransactionResponseDto400' + '500': + description: Error interno de servidor + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + examples: + CreateTransactionResponseDto500: + $ref: '#/components/examples/CreateTransactionResponseDto500' + + /transaction/{transactionExternalId}: + get: + summary: Recuperar una transacción para revisar su estado + operationId: retrieveTransaction + tags: + - Transaction + parameters: + - name: transactionExternalId + in: path + required: true + description: UUID externo de la transacción + schema: + type: string + format: uuid + example: e6f9433b-a681-4352-a1c3-2ebe1126683f + responses: + '200': + description: Transacción recuperada exitosamente + content: + application/json: + schema: + $ref: '#/components/schemas/RetrieveTransactionResponseDto' + examples: + RetrieveTransactionResponseDto200: + $ref: '#/components/examples/RetrieveTransactionResponseDto200' + '400': + description: Identificador de la transacción externa inválido + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + examples: + RetrieveTransactionResponseDto400: + $ref: '#/components/examples/RetrieveTransactionResponseDto400' + '404': + description: Transacción no encontrada + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + examples: + RetrieveTransactionResponseDto404: + $ref: '#/components/examples/RetrieveTransactionResponseDto404' + '500': + description: Error interno de servidor + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponseDto' + examples: + CreateTransactionResponseDto500: + $ref: '#/components/examples/CreateTransactionResponseDto500' + +components: + schemas: + CreateTransactionRequestDto: + type: object + required: + - accountExternalIdDebit + - accountExternalIdCredit + - transactionTypeId + - value + properties: + accountExternalIdDebit: + type: string + format: uuid + description: Identificador de la cuenta externa de donde se debita + accountExternalIdCredit: + type: string + format: uuid + description: Identificador de la cuenta externa de donde se acredita + transactionTypeId: + type: integer + description: "Identificador del tipo de transacción. 0: deposito, 1: retiro" + enum: [0, 1] + example: 1 + value: + type: number + format: decimal + description: Valor de la transacción + example: 1001 + + CreateTransactionResponseDto: + type: object + properties: + transactionExternalId: + type: string + format: uuid + description: Identificador de la transacción externa + example: e6f9433b-a681-4352-a1c3-2ebe1126683f + transactionType: + $ref: '#/components/schemas/TransactionTypeDto' + transactionStatus: + $ref: '#/components/schemas/TransactionStatusDto' + value: + type: number + format: decimal + example: 1001 + createdAt: + type: string + format: date-time + description: Fecha de creación de la transacción + example: "2026-02-25T23:00:00Z" + + RetrieveTransactionResponseDto: + type: object + properties: + transactionExternalId: + type: string + format: uuid + example: e6f9433b-a681-4352-a1c3-2ebe1126683f + transactionType: + $ref: '#/components/schemas/TransactionTypeDto' + transactionStatus: + $ref: '#/components/schemas/TransactionStatusDto' + value: + type: number + format: decimal + description: Valor de la transacción + example: 1001 + createdAt: + type: string + format: date-time + description: Fecha de creación de la transacción + example: "2026-02-25T23:00:00Z" + + TransactionTypeDto: + type: object + description: Tipo de transacción + properties: + name: + type: string + description: Nombre del tipo de transacción + enum: + - DEPOSIT + - WITHDRAWAL + example: "WITHDRAWAL" + + TransactionStatusDto: + type: object + description: Estado de la transacción + properties: + name: + type: string + description: Nombre del estado de la transacción + enum: + - PENDING + - APPROVED + - REJECTED + example: "REJECTED" + + ErrorResponseDto: + type: object + description: Error generico sin información sensible. Los datos más detallados sobre el error se encuentran en los logs. + properties: + message: + type: string + description: Mensaje del error + example: Error interno del servidor + + examples: + CreateTransactionRequestDto200: + summary: Petición de creación de transacción exitosa + description: Ejemplo de una transferencia de fondos entre dos cuentas externas. + value: + accountExternalIdDebit: "550e8400-e29b-41d4-a716-446655440000" + accountExternalIdCredit: "6ba7b810-9dad-11d1-80b4-00c04fd430c8" + transactionTypeId: 1 + value: 120.50 + CreateTransactionResponseDto200: + summary: Respuesta de transacción creada (Pendiente) + description: La transacción ha sido registrada y está a la espera de validación antifraude. + value: + transactionExternalId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" + transactionType: + name: "WITHDRAWAL" + transactionStatus: + name: "PENDING" + value: 120.50 + createdAt: "2026-02-25T14:30:00.000Z" + RetrieveTransactionResponseDto200: + summary: Transacción aprobada + description: Ejemplo de una transacción que ya pasó por el microservicio de antifraude y fue aprobada. + value: + transactionExternalId: "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11" + transactionType: + name: "WITHDRAWAL" + transactionStatus: + name: "APPROVED" + value: 120.50 + createdAt: "2026-02-25T14:32:00.000Z" + + CreateTransactionRequestDto400: + summary: Petición de creación con campo transactionTypeId inválido + description: Ejemplo con el campo transactionTypeId con valor 3 + value: + accountExternalIdDebit: "550e8400-e29b-41d4-a716-446655440000" + accountExternalIdCredit: "6ba7b810-9dad-11d1-80b4-00c04fd430c8" + transactionTypeId: 3 + value: 120.50 + CreateTransactionResponseDto400: + summary: Respuesta informando que los campos enviados en la petición son inválidos. + description: Ejemplo con el campo transactionTypeId con valor 3 + value: + message: Campos inválidos + + RetrieveTransactionResponseDto400: + summary: Identificador de la transacción externa inválido + description: Identificador de la transacción externa inválido + value: + message: Identificador de la transacción externa inválido + RetrieveTransactionResponseDto404: + summary: Transacción no encontrada + description: La Transacción no se encuentra en la base de datos + value: + message: Transacción Externa no encontrada + + CreateTransactionResponseDto500: + summary: Error interno del servidor + description: Algún servicio interno está caído + value: + message: Error interno del servidor + RetrieveTransactionResponseDto500: + summary: Error interno del servidor + description: Algún servicio interno está caído + value: + message: Error interno del servidor