diff --git a/.gitignore b/.gitignore index 67045665db..d545169072 100644 --- a/.gitignore +++ b/.gitignore @@ -1,104 +1,39 @@ -# Logs -logs +# ===== Java ===== +*.class +*.log +*.ctxt + +# ===== Maven ===== +target/ +!.mvn/wrapper/maven-wrapper.jar + +# ===== Spring Boot ===== +*.jar +*.war +*.ear +*.iml + +# ===== IDEs ===== +.idea/ +.vscode/ +*.swp +*.swo + +# ===== OS ===== +.DS_Store +Thumbs.db + +# ===== 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 +# ===== Environment ===== .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/ +.env.local -# DynamoDB Local files -.dynamodb/ +# ===== Docker ===== +**/docker-data/ -# TernJS port file -.tern-port +# ===== Kafka (local) ===== +kafka-data/ +zookeeper-data/ diff --git a/Dockerfile.antifraud b/Dockerfile.antifraud new file mode 100644 index 0000000000..8c10861a46 --- /dev/null +++ b/Dockerfile.antifraud @@ -0,0 +1,31 @@ +# ===== Etapa de build ===== +FROM maven:3.9.12-eclipse-temurin-21-alpine AS build + +WORKDIR /antifraud + +# Copiar POM directamente al WORKDIR +COPY ./yape-financial/yape-antifraud/pom.xml ./pom.xml + +# Descargar dependencias sin compilar (aprovecha cache de Docker) +RUN mvn dependency:go-offline -B + +# Copiar el código fuente +COPY ./yape-financial/yape-antifraud/src ./src + +# Compilar microservicio (tests omitidos) +RUN mvn clean package -DskipTests=true + + +# ===== Etapa runtime ===== +FROM eclipse-temurin:21-jdk-alpine + +WORKDIR /antifraud + +# Copiar jar construido desde la etapa de build +COPY --from=build /antifraud/target/yape-antifraud-1.0-SNAPSHOT.jar . + +# Exponer puerto del microservicio +EXPOSE 8082 + +# Ejecutar la aplicación +ENTRYPOINT ["java", "-jar", "yape-antifraud-1.0-SNAPSHOT.jar"] diff --git a/Dockerfile.transaction b/Dockerfile.transaction new file mode 100644 index 0000000000..11c3ee072b --- /dev/null +++ b/Dockerfile.transaction @@ -0,0 +1,27 @@ +# ===== Etapa de build ===== +FROM maven:3.9.12-eclipse-temurin-21-alpine AS build + +# Directorio de trabajo dentro del contenedor +WORKDIR /transaction + +# Copiar POM y código fuente directamente al WORKDIR +COPY ./yape-financial/yape-transaction/pom.xml ./pom.xml +COPY ./yape-financial/yape-transaction/src ./src + +# Compilar el microservicio (skip tests para acelerar) +RUN mvn clean package -DskipTests=true + + +# ===== Etapa runtime ===== +FROM eclipse-temurin:21-jdk + +WORKDIR /transaction + +# Copiar jar construido desde la etapa de build +COPY --from=build /transaction/target/yape-transaction-1.0-SNAPSHOT.jar . + +# Exponer puerto del microservicio +EXPOSE 8081 + +# Ejecutar la aplicación +ENTRYPOINT ["java", "-jar", "yape-transaction-1.0-SNAPSHOT.jar"] diff --git a/README.md b/README.md index b067a71026..fbd955ec07 100644 --- a/README.md +++ b/README.md @@ -1,82 +1,124 @@ -# Yape Code Challenge :rocket: -Our code challenge will let you marvel us with your Jedi coding skills :smile:. +## APLICACIÓN DE TRANSACCIONES: ```YAPE-FINANCIAL``` +Aplicación de microservicios para manejo de transacciones financieras con validación antifraude y comunicación asíncrona mediante Kafka. + +### ANTIFRAUDE + +- Al crear una transacción, se publica un evento en Kafka. +- Un consumidor valida reglas antifraude. +- La transacción puede ser aprobada o rechazada según el monto configurado. + +### REQUISITOS + +- Docker >= 27.x +- Docker Compose >= 2.x + +### COMPONENTES + +- JAVA: 21 +- SPRING BOOT: 3.2.5 +- DOCKER +- KAFKA +- POSTGRESQL + +### INSTALACIÓN + +- Clonar el repositorio: + +```bash + git clone https://github.com/evercarlos/app-nodejs-codechallenge.git + ``` + - Entrar a la rama del proyecto: + ```bash + git checkout java-codechallenge + ``` + +- Construir y levantar los contenedores con Docker Compose + + ```bash + docker-compose up --build -d + ``` + +### CONFIGURACIÓN + +Las variables de entorno principales se encuentran en el archivo `docker-compose.yml`: +- Base de datos PostgreSQL +- Kafka +- Puertos de exposición + +### URL DEL MICROSERVICIO Y DOCUMENTACIÓN SWAGGER +- http://localhost:8081 +- Swagger UI: http://localhost:8081/swagger-ui.html + +### ARQUITECTURA + +La aplicación sigue una arquitectura hexagonal (Ports & Adapters), separando: +- Aplication +- Domain +- Infraestructure (REST, Kafka, persistencia) + +La validación antifraude se realiza de forma desacoplada mediante eventos publicados en Kafka. + +### ENDPOINTS DISPONIBLES +1. Crea una transacción + + ```bash +curl --location 'http://localhost:8081/api/v1/transaction' \ +--header 'Content-Type: application/json' \ +--data '{ + "accountExternalIdDebit": "550e8400-e29b-41d4-a716-446655440000", + "accountExternalIdCredit": "660e8400-e29b-41d4-a716-446655440111", + "tranferTypeId": 1, + "value": 10001 + } ' + ``` + + 2. Lista transacción con paginación + + ```bash +curl --location 'http://localhost:8081/api/v1/transaction/withPagination?number=1&size=10' \ +--header 'Content-Type: application/json' \ +--data '' + ``` + 3. Lista transacción sin paginación + + ```bash +curl --location 'http://localhost:8081/api/v1/transaction' \ +--header 'Content-Type: application/json' \ +--data '' + ``` + + 4. Busca una transaccion por transactionExternalId + + ```bash +curl --location 'http://localhost:8081/api/v1/transaction/{transactionExternalId}' \ +--header 'Content-Type: application/json' \ +--data '' + ``` + ### FLUJO DE LA TRANSACCIÓN + +#### Creación de una transacción +1. El cliente crea una transacción mediante el endpoint: + `[POST] /api/v1/transaction`. +2. La transacción se persiste inicialmente con estado `PENDIENTE` en la base de datos PostgreSQL. +3. Se publica un evento de transacción en un tópico de Kafka para su validación antifraude. +4. El microservicio de antifraude consume el evento y valida las reglas de negocio. +5. Como resultado de la validación, la transacción es: + - `APROBADO` si cumple las reglas. + - `RECHAZADO` si no cumple las reglas. +6. El estado final de la transacción se actualiza en la base de datos. + +#### Consulta de transacciones +- Las transacciones pueden consultarse: + - Con paginación. + - Sin paginación. + - Por `transactionExternalId`. + + ### DIAGRAMA + +![](./resources/arq.png) + +### NOTA +- Para escenarios de alto volumen y concurrencia, una alternativa sería Cassandra, donde el modelo estaría orientado a las +consultas y permitiría alta disponibilidad y escalabilidad horizontal. -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. -For now, we have only three transaction statuses: - -
    -
  1. pending
  2. -
  3. approved
  4. -
  5. rejected
  6. -
- -Every transaction with a value greater than 1000 should be rejected. - -```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)] -``` - -# Tech Stack - -
    -
  1. Node. You can use any framework you want (i.e. Nestjs with an ORM like TypeOrm or Prisma)
  2. -
  3. Any database
  4. -
  5. Kafka
  6. -
- -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 -{ - "accountExternalIdDebit": "Guid", - "accountExternalIdCredit": "Guid", - "tranferTypeId": 1, - "value": 120 -} -``` - -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. diff --git a/docker-compose.yml b/docker-compose.yml index 0e8807f21c..3671fe764f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,81 @@ -version: "3.7" +version: "3.9" + +networks: + yape_challenge: + services: - postgres: - image: postgres:14 - ports: - - "5432:5432" - environment: - - POSTGRES_USER=postgres - - POSTGRES_PASSWORD=postgres zookeeper: - image: confluentinc/cp-zookeeper:5.5.3 + image: confluentinc/cp-zookeeper:7.4.0 + container_name: zookeeper environment: ZOOKEEPER_CLIENT_PORT: 2181 + ZOOKEEPER_TICK_TIME: 2000 + ports: + - "2181:2181" + networks: + - yape_challenge + kafka: - image: confluentinc/cp-enterprise-kafka:5.5.3 - depends_on: [zookeeper] + image: confluentinc/cp-kafka:7.4.0 + container_name: kafka + depends_on: + - zookeeper + ports: + - "9092:9092" environment: - KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181" - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT KAFKA_BROKER_ID: 1 + 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_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_JMX_PORT: 9991 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + restart: always + networks: + - yape_challenge + + yape-transaction: + build: + context: . + dockerfile: Dockerfile.transaction + container_name: yape-transaction + environment: + SPRING_DATASOURCE_URL: jdbc:postgresql://postgres_yape:5432/dbtransactionv1 + SPRING_DATASOURCE_USERNAME: postgres + SPRING_DATASOURCE_PASSWORD: postgres + KAFKA_BOOTSTRAP_SERVERS: kafka:29092 + ports: + - "8081:8081" + depends_on: + - kafka + - postgres_yape + networks: + - yape_challenge + + yape-antifraud: + build: + context: . + dockerfile: Dockerfile.antifraud + container_name: yape-antifraud + environment: + KAFKA_BOOTSTRAP_SERVERS: kafka:29092 ports: - - 9092:9092 + - "8082:8082" + depends_on: + - kafka + networks: + - yape_challenge + + postgres_yape: + image: postgres:14 + container_name: postgres_yape + ports: + - "5433:5432" + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: dbtransactionv1 + volumes: + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - yape_challenge diff --git a/init.sql b/init.sql new file mode 100644 index 0000000000..cd606ff5cd --- /dev/null +++ b/init.sql @@ -0,0 +1,48 @@ +-- =============================== +-- CONNECT TO POSTGRES +-- =============================== +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_database WHERE datname = 'dbtransactionv1' + ) THEN + CREATE DATABASE dbtransactionv1; + END IF; +END +$$; + +-- CONNECT +\c dbtransactionv1 + +-- EXTENSION PARA UUID +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +-- TABLA: TRANSACTION +CREATE TABLE IF NOT EXISTS public.transacciones +( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + transaction_external_id UUID NOT NULL UNIQUE, + account_external_id_debit UUID NOT NULL, + account_external_id_credit UUID NOT NULL, + transfer_type_id INTEGER NOT NULL, + transaction_status VARCHAR(20) NOT NULL, + value NUMERIC(15, 2) NOT NULL, + created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT now() +); + +-- INDICES +CREATE INDEX IF NOT EXISTS idx_transacciones_external_id +ON public.transacciones (transaction_external_id); + +CREATE INDEX IF NOT EXISTS idx_transacciones_status +ON public.transacciones (transaction_status); + +CREATE INDEX IF NOT EXISTS idx_transacciones_created_at +ON public.transacciones (created_at); + +-- =============================== +-- PERMISSIONS +-- =============================== +GRANT CONNECT ON DATABASE dbtransactionv1 TO PUBLIC; +GRANT ALL PRIVILEGES ON DATABASE dbtransactionv1 TO postgres; +GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO postgres; diff --git a/resources/arq.png b/resources/arq.png new file mode 100644 index 0000000000..e9744e99b4 Binary files /dev/null and b/resources/arq.png differ diff --git a/yape-financial/yape-antifraud/.gitignore b/yape-financial/yape-antifraud/.gitignore new file mode 100644 index 0000000000..5ff6309b71 --- /dev/null +++ b/yape-financial/yape-antifraud/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/yape-financial/yape-antifraud/pom.xml b/yape-financial/yape-antifraud/pom.xml new file mode 100644 index 0000000000..a78d33e6d2 --- /dev/null +++ b/yape-financial/yape-antifraud/pom.xml @@ -0,0 +1,152 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + com.tec.yape.antifraud + yape-antifraud + jar + 1.0-SNAPSHOT + + + 21 + 21 + UTF-8 + 1.6.3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.projectlombok + lombok + true + + + org.mapstruct + mapstruct + ${org.mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + provided + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + org.springdoc + springdoc-openapi-data-rest + 1.6.4 + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.code.gson + gson + 2.8.9 + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-cache + + + + org.springframework.boot + spring-boot-starter-test + + + org.mockito + mockito-junit-jupiter + 5.11.0 + test + + + org.springframework.retry + spring-retry + 1.3.1 + + + org.springframework.kafka + spring-kafka + + + org.springframework.kafka + spring-kafka-test + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + + --enable-preview + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + --enable-preview + + + + org.springframework.boot + spring-boot-maven-plugin + + --enable-preview + + + + + + \ No newline at end of file diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/YapeAntiFraudService.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/YapeAntiFraudService.java new file mode 100644 index 0000000000..a19f5f98f8 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/YapeAntiFraudService.java @@ -0,0 +1,13 @@ +package com.tec.yape.antifraud; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; + +@SpringBootApplication +@EnableAsync +public class YapeAntiFraudService { + public static void main(String[] args) { + SpringApplication.run(YapeAntiFraudService.class, args); + } +} \ No newline at end of file diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/application/usecase/FraudValidationUseCase.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/application/usecase/FraudValidationUseCase.java new file mode 100644 index 0000000000..d30081476b --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/application/usecase/FraudValidationUseCase.java @@ -0,0 +1,37 @@ +package com.tec.yape.antifraud.application.usecase; + +import com.tec.yape.antifraud.domain.enums.TransactionStatus; +import com.tec.yape.antifraud.domain.model.TransactionCreatedEvent; +import com.tec.yape.antifraud.domain.model.TransactionValidatedEvent; +import com.tec.yape.antifraud.domain.port.input.FraudValidationInPort; +import com.tec.yape.antifraud.domain.port.output.TransactionStatusPublisherOutPort; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; + +@Component +@RequiredArgsConstructor +@Slf4j +public class FraudValidationUseCase implements FraudValidationInPort { + + private final TransactionStatusPublisherOutPort statusPublisher; + + @Override + public void validate(TransactionCreatedEvent event) { + + + TransactionStatus status = + event.value().compareTo(BigDecimal.valueOf(1000)) > 0 + ? TransactionStatus.RECHAZADO + : TransactionStatus.APROBADO; + + statusPublisher.publish( + new TransactionValidatedEvent( + event.transactionExternalId(), + status + ) + ); + } +} \ No newline at end of file diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/enums/TransactionStatus.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/enums/TransactionStatus.java new file mode 100644 index 0000000000..4197c41643 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/enums/TransactionStatus.java @@ -0,0 +1,7 @@ +package com.tec.yape.antifraud.domain.enums; + +public enum TransactionStatus { + PENDIENTE, + APROBADO, + RECHAZADO +} \ No newline at end of file diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/exception/CommonErrorType.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/exception/CommonErrorType.java new file mode 100644 index 0000000000..0cff88117a --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/exception/CommonErrorType.java @@ -0,0 +1,16 @@ +package com.tec.yape.antifraud.domain.exception; + +import lombok.Getter; + +@Getter +public enum CommonErrorType { + + COMMON_ERROR_400_1("Error en el criterio de ordenación"); + + private final String description; + + + CommonErrorType(String description) { + this.description = description; + } +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/exception/TransactionException.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/exception/TransactionException.java new file mode 100644 index 0000000000..a78778f26a --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/exception/TransactionException.java @@ -0,0 +1,18 @@ +package com.tec.yape.antifraud.domain.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.http.HttpStatus; + +@EqualsAndHashCode(callSuper = true) +@Data +public class TransactionException extends RuntimeException { + + public HttpStatus status; + public String code; + + public TransactionException(HttpStatus httpStatus, String message) { + super(message); + this.status = httpStatus; + } +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/model/Transaction.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/model/Transaction.java new file mode 100644 index 0000000000..64c385a5ab --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/model/Transaction.java @@ -0,0 +1,23 @@ +package com.tec.yape.antifraud.domain.model; + +import com.tec.yape.antifraud.domain.enums.TransactionStatus; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +public class Transaction { + + private UUID transactionExternalId; + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private Integer transferTypeId; + private BigDecimal value; + private TransactionStatus transactionStatus; + private LocalDateTime createdAt; + +} \ No newline at end of file diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/model/TransactionCreatedEvent.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/model/TransactionCreatedEvent.java new file mode 100644 index 0000000000..79c2a6eefa --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/model/TransactionCreatedEvent.java @@ -0,0 +1,9 @@ +package com.tec.yape.antifraud.domain.model; + +import java.math.BigDecimal; +import java.util.UUID; + +public record TransactionCreatedEvent( + UUID transactionExternalId, + BigDecimal value +) {} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/model/TransactionValidatedEvent.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/model/TransactionValidatedEvent.java new file mode 100644 index 0000000000..7d4b2aac55 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/model/TransactionValidatedEvent.java @@ -0,0 +1,11 @@ +package com.tec.yape.antifraud.domain.model; + + +import com.tec.yape.antifraud.domain.enums.TransactionStatus; + +import java.util.UUID; + +public record TransactionValidatedEvent( + UUID transactionExternalId, + TransactionStatus status +) {} \ No newline at end of file diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/port/input/FraudValidationInPort.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/port/input/FraudValidationInPort.java new file mode 100644 index 0000000000..dc8046c11b --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/port/input/FraudValidationInPort.java @@ -0,0 +1,8 @@ +package com.tec.yape.antifraud.domain.port.input; + +import com.tec.yape.antifraud.domain.model.TransactionCreatedEvent; + +public interface FraudValidationInPort { + + void validate(TransactionCreatedEvent event); +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/port/output/TransactionStatusPublisherOutPort.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/port/output/TransactionStatusPublisherOutPort.java new file mode 100644 index 0000000000..5baf68493f --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/port/output/TransactionStatusPublisherOutPort.java @@ -0,0 +1,8 @@ +package com.tec.yape.antifraud.domain.port.output; + +import com.tec.yape.antifraud.domain.model.TransactionValidatedEvent; + +public interface TransactionStatusPublisherOutPort { + + void publish(TransactionValidatedEvent event); +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/util/BeanConstants.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/util/BeanConstants.java new file mode 100644 index 0000000000..01bcd664a3 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/util/BeanConstants.java @@ -0,0 +1,8 @@ +package com.tec.yape.antifraud.domain.util; + +public class BeanConstants { + + public static final String ASYNC_SAVE_CALL_HISTORY = "asyncSaveCallHistory"; + + public static final String ASYNC_VIRTUAL_SAVE_CALL_HISTORY = "asyncVirtualSaveCallHistory"; +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/util/JsonUtil.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/util/JsonUtil.java new file mode 100644 index 0000000000..ef6901a919 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/util/JsonUtil.java @@ -0,0 +1,40 @@ +package com.tec.yape.antifraud.domain.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +public class JsonUtil { + + + private JsonUtil() { + + } + + public static String ToJSON(Object object) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.registerModule(new Jdk8Module()); + mapper.registerModule(new JavaTimeModule()); + return mapper.writeValueAsString(object); + } + + public static T fromJson(String message, Class tClass) { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.registerModule(new Jdk8Module()); + mapper.registerModule(new JavaTimeModule()); + + try { + return mapper.readValue(message, tClass); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/util/SubscriberResponse.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/util/SubscriberResponse.java new file mode 100644 index 0000000000..7d6e3817fb --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/domain/util/SubscriberResponse.java @@ -0,0 +1,7 @@ +package com.tec.yape.antifraud.domain.util; + +public enum SubscriberResponse { + + FINALIZED, + ERRORS +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/adapter/TransactionStatusPublisherAdapter.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/adapter/TransactionStatusPublisherAdapter.java new file mode 100644 index 0000000000..d7d5cd536c --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/adapter/TransactionStatusPublisherAdapter.java @@ -0,0 +1,22 @@ +package com.tec.yape.antifraud.infrastructure.adapter; + +import com.tec.yape.antifraud.domain.model.TransactionValidatedEvent; +import com.tec.yape.antifraud.domain.port.output.TransactionStatusPublisherOutPort; +import com.tec.yape.antifraud.infrastructure.messaging.producer.TransactionStatusPublisher; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + + +@Component +@RequiredArgsConstructor +@Slf4j +public class TransactionStatusPublisherAdapter implements TransactionStatusPublisherOutPort { + + private final TransactionStatusPublisher transactionStatusPublisher; + + @Override + public void publish(TransactionValidatedEvent event) { + transactionStatusPublisher.publish(event); + } +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/ExecutorAsyncVirtualConfig.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/ExecutorAsyncVirtualConfig.java new file mode 100644 index 0000000000..fc13421b7f --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/ExecutorAsyncVirtualConfig.java @@ -0,0 +1,19 @@ +package com.tec.yape.antifraud.infrastructure.config; + +import com.tec.yape.antifraud.domain.util.BeanConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +@Configuration +public class ExecutorAsyncVirtualConfig { + + @Bean(name = BeanConstants.ASYNC_VIRTUAL_SAVE_CALL_HISTORY) + public Executor asyncVirtualSaveCallHistory() { + return Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("virtual-thread-", 0)::unstarted + ); + } +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/GlobalExceptionHandler.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/GlobalExceptionHandler.java new file mode 100644 index 0000000000..e2bae4e6af --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/GlobalExceptionHandler.java @@ -0,0 +1,19 @@ +package com.tec.yape.antifraud.infrastructure.config; + +import com.tec.yape.antifraud.infrastructure.config.dto.ErrorDto; +import com.tec.yape.antifraud.domain.exception.TransactionException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(value = TransactionException.class) + public ResponseEntity businessExceptionHandler(TransactionException ex) { + ErrorDto error = ErrorDto.builder() + .status(ex.getStatus()) + .code(ex.getCode()).message(ex.getMessage()).build(); + return new ResponseEntity<>(error, ex.getStatus()); + } +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/OpenApicConfig.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/OpenApicConfig.java new file mode 100644 index 0000000000..61c9c6859d --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/OpenApicConfig.java @@ -0,0 +1,35 @@ +package com.tec.yape.antifraud.infrastructure.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.springframework.context.annotation.Configuration; + +@Configuration +@OpenAPIDefinition( + info = @Info( + title = "API-REST ANTIFRAUDE", + version = "1.0", + + description = "Api Rest para Antifraude", + + license = @License( + name = "Apache 2.0", + url = "http://www.apache.org/licenses/LICENSE-2.0.html" + ), + + contact = @Contact( + name = "tec.com", + url = "tec.com", + email = "evercarlosrojas@gmail.com") + ) +) +@SecurityScheme(name = "bearerAuth", type = SecuritySchemeType.HTTP, bearerFormat = "JWT", description = "Autenticación" + + "tipo Bearer API-TC", scheme = "bearer") +public class OpenApicConfig { + + +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/dto/ErrorDto.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/dto/ErrorDto.java new file mode 100644 index 0000000000..844161ad11 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/dto/ErrorDto.java @@ -0,0 +1,13 @@ +package com.tec.yape.antifraud.infrastructure.config.dto; + +import lombok.Builder; +import lombok.Data; +import org.springframework.http.HttpStatus; + +@Data +@Builder +public class ErrorDto { + private String code; + private String message; + private HttpStatus status; +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/kafka/consumer/KafkaConsumerConfig.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/kafka/consumer/KafkaConsumerConfig.java new file mode 100644 index 0000000000..12d6783b66 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/kafka/consumer/KafkaConsumerConfig.java @@ -0,0 +1,44 @@ +package com.tec.yape.antifraud.infrastructure.config.kafka.consumer; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.config.KafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.listener.ConcurrentMessageListenerContainer; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaConsumerConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String boostrapServers; + + + public Map consumerConfig() { + Map properties = new HashMap<>(); + properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, boostrapServers); + properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringSerializer.class);// deserializa: convirte de string a bytes + properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringSerializer.class);// deserializa + return properties; + } + + @Bean + public ConsumerFactory consumerFactory() { + return new DefaultKafkaConsumerFactory<>(consumerConfig()); + } + + @Bean // Para poder inyectar en otros lugares + public KafkaListenerContainerFactory> consumer() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + return factory; + } + +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/kafka/producer/KafkaProducerConfig.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/kafka/producer/KafkaProducerConfig.java new file mode 100644 index 0000000000..10ba7bc06f --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/config/kafka/producer/KafkaProducerConfig.java @@ -0,0 +1,41 @@ +package com.tec.yape.antifraud.infrastructure.config.kafka.producer; + +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaProducerConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String boostrapServers; + + + public Map producerConfig() { + Map properties = new HashMap<>(); + properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, boostrapServers); + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);// serializa: convirte de string a bytes + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);// serializa + return properties; + } + + @Bean + public ProducerFactory providerFactory() { + return new DefaultKafkaProducerFactory<>(producerConfig()); + } + + // Envian mensaje + @Bean + public KafkaTemplate kafkaTemplate(ProducerFactory providerFactory) {// inyectando + return new KafkaTemplate<>(providerFactory); + } + +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/messaging/consuper/TransactionEventConsumer.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/messaging/consuper/TransactionEventConsumer.java new file mode 100644 index 0000000000..5df3c2a83c --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/messaging/consuper/TransactionEventConsumer.java @@ -0,0 +1,38 @@ +package com.tec.yape.antifraud.infrastructure.messaging.consuper; + +import com.tec.yape.antifraud.domain.port.input.FraudValidationInPort; +import com.tec.yape.antifraud.domain.util.JsonUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.stereotype.Component; +import com.tec.yape.antifraud.domain.model.TransactionCreatedEvent; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +@Component +@Slf4j +@RequiredArgsConstructor +public class TransactionEventConsumer { + + private final FraudValidationInPort fraudValidationInPort; + + @KafkaListener( + topics = "topic-transaction", + groupId = "antifraud-service-group", + containerFactory = "kafkaListenerContainerFactory" + ) + public void listener(String message, @Headers Map headers) { + + String jsonMessage = new String(message.getBytes(), StandardCharsets.UTF_8); + + TransactionCreatedEvent event = JsonUtil.fromJson(jsonMessage, TransactionCreatedEvent.class); + + log.info("[AntifraudConsumer] Transaction received: {}", jsonMessage); + log.info("[AntifraudConsumer] Client-id header: {}", headers.get("client-id")); + + fraudValidationInPort.validate(event); + } +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/messaging/producer/TransactionStatusPublisher.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/messaging/producer/TransactionStatusPublisher.java new file mode 100644 index 0000000000..cdadaec8c1 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/messaging/producer/TransactionStatusPublisher.java @@ -0,0 +1,8 @@ +package com.tec.yape.antifraud.infrastructure.messaging.producer; + +import com.tec.yape.antifraud.domain.model.TransactionValidatedEvent; + +public interface TransactionStatusPublisher { + + void publish(TransactionValidatedEvent message); +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/messaging/producer/TransactionStatusPublisherImpl.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/messaging/producer/TransactionStatusPublisherImpl.java new file mode 100644 index 0000000000..a48e17d324 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/messaging/producer/TransactionStatusPublisherImpl.java @@ -0,0 +1,52 @@ +package com.tec.yape.antifraud.infrastructure.messaging.producer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.tec.yape.antifraud.domain.model.TransactionValidatedEvent; +import com.tec.yape.antifraud.domain.util.JsonUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.KafkaHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +@Slf4j +public class TransactionStatusPublisherImpl implements TransactionStatusPublisher { + + private final KafkaTemplate kafkaTemplate; + + public TransactionStatusPublisherImpl(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Override + public void publish(TransactionValidatedEvent event) { + log.info("[TransactionStatusPublisherImpl] {}", "Start async process"); + + try { + String message = JsonUtil.ToJSON(event); + + Message kafkaMessage = MessageBuilder + .withPayload(message) + .setHeader(KafkaHeaders.TOPIC, "topic-transaction-validated") + .copyHeaders(getHeaders()) + .build(); + + kafkaTemplate.send(kafkaMessage); + + } catch (JsonProcessingException e) { + log.error("[TransactionStatusPublisherImpl] message:{}", e.getMessage()); + } + } + + private Map getHeaders() { + Map headers = new HashMap<>(); + headers.put("client-id", "EVER CARLOS ROJAS"); + return headers; + } + +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/controller/IndexController.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/controller/IndexController.java new file mode 100644 index 0000000000..2d41ab515b --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/controller/IndexController.java @@ -0,0 +1,13 @@ +package com.tec.yape.antifraud.infrastructure.rest.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class IndexController { + + @RequestMapping("/") + public String getIndex(){ + return "redirect:swagger-ui.html"; + } +} \ No newline at end of file diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/request/TransactionRequestDto.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/request/TransactionRequestDto.java new file mode 100644 index 0000000000..900c863e75 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/request/TransactionRequestDto.java @@ -0,0 +1,25 @@ +package com.tec.yape.antifraud.infrastructure.rest.dto.request; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.util.UUID; + +@Getter +@Setter +public class TransactionRequestDto { + + private UUID accountExternalIdDebit; + + private UUID accountExternalIdCredit; + + @JsonProperty("tranferTypeId") + private Integer transferTypeId; + + private BigDecimal value; + + +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/response/TransactionResponseDto.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/response/TransactionResponseDto.java new file mode 100644 index 0000000000..a00bb0fff1 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/response/TransactionResponseDto.java @@ -0,0 +1,24 @@ +package com.tec.yape.antifraud.infrastructure.rest.dto.response; + +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Setter +@Getter +public class TransactionResponseDto { + + private UUID transactionExternalId; + + private TransactionTypeDto transactionType; + + private TransactionStatusDto transactionStatus; + + private BigDecimal value; + + private LocalDateTime createdAt; + +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/response/TransactionStatusDto.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/response/TransactionStatusDto.java new file mode 100644 index 0000000000..bfbe58df71 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/response/TransactionStatusDto.java @@ -0,0 +1,16 @@ +package com.tec.yape.antifraud.infrastructure.rest.dto.response; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class TransactionStatusDto { + + String name; + + public TransactionStatusDto(String name) { + this.name = name; + } + +} diff --git a/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/response/TransactionTypeDto.java b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/response/TransactionTypeDto.java new file mode 100644 index 0000000000..647447e8fd --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/java/com/tec/yape/antifraud/infrastructure/rest/dto/response/TransactionTypeDto.java @@ -0,0 +1,16 @@ +package com.tec.yape.antifraud.infrastructure.rest.dto.response; + +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +public class TransactionTypeDto { + + String name; + + public TransactionTypeDto(String name) { + this.name = name; + } + +} diff --git a/yape-financial/yape-antifraud/src/main/resources/application.properties b/yape-financial/yape-antifraud/src/main/resources/application.properties new file mode 100644 index 0000000000..8293ace2a1 --- /dev/null +++ b/yape-financial/yape-antifraud/src/main/resources/application.properties @@ -0,0 +1,8 @@ +server.tomcat.uri-encoding=UTF-8 +server.port=8082 + +# settings KAFKA +#spring.kafka.bootstrap-servers = localhost:9092 +spring.kafka.bootstrap-servers=kafka:9092 + + diff --git a/yape-financial/yape-antifraud/src/test/java/com/tec/antifraud/FraudValidationUseCaseTest.java b/yape-financial/yape-antifraud/src/test/java/com/tec/antifraud/FraudValidationUseCaseTest.java new file mode 100644 index 0000000000..522c993588 --- /dev/null +++ b/yape-financial/yape-antifraud/src/test/java/com/tec/antifraud/FraudValidationUseCaseTest.java @@ -0,0 +1,51 @@ +package com.tec.antifraud; + +import com.tec.yape.antifraud.application.usecase.FraudValidationUseCase; +import com.tec.yape.antifraud.domain.enums.TransactionStatus; +import com.tec.yape.antifraud.domain.model.TransactionCreatedEvent; +import com.tec.yape.antifraud.domain.port.output.TransactionStatusPublisherOutPort; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.util.UUID; + +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class FraudValidationUseCaseTest { + + @Mock + private TransactionStatusPublisherOutPort publisher; + + @InjectMocks + private FraudValidationUseCase useCase; + + @Test + void shouldApproveTransactionWhenValueIsLessOrEqualThan1000() { + TransactionCreatedEvent event = new TransactionCreatedEvent( + UUID.randomUUID(), + new BigDecimal("1000") + ); + + useCase.validate(event); + + verify(publisher).publish(argThat(e -> e.status().equals(TransactionStatus.APROBADO))); + } + + @Test + void shouldRejectTransactionWhenValueIsGreaterThan1000() { + TransactionCreatedEvent event = new TransactionCreatedEvent( + UUID.randomUUID(), + new BigDecimal("1500") + ); + + useCase.validate(event); + + verify(publisher).publish(argThat(e -> e.status().equals(TransactionStatus.RECHAZADO))); + } +} diff --git a/yape-financial/yape-antifraud/src/test/resources/application-test.properties b/yape-financial/yape-antifraud/src/test/resources/application-test.properties new file mode 100644 index 0000000000..e69de29bb2 diff --git a/yape-financial/yape-transaction/.gitignore b/yape-financial/yape-transaction/.gitignore new file mode 100644 index 0000000000..5ff6309b71 --- /dev/null +++ b/yape-financial/yape-transaction/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/yape-financial/yape-transaction/pom.xml b/yape-financial/yape-transaction/pom.xml new file mode 100644 index 0000000000..9dc16da926 --- /dev/null +++ b/yape-financial/yape-transaction/pom.xml @@ -0,0 +1,161 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + + com.tec.yape.transaction + yape-transaction + jar + 1.0-SNAPSHOT + + + 21 + 21 + UTF-8 + 1.6.3 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + runtime + + + org.projectlombok + lombok + true + + + org.mapstruct + mapstruct + ${org.mapstruct.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + provided + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.3.0 + + + org.springdoc + springdoc-openapi-data-rest + 1.6.4 + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.code.gson + gson + 2.8.9 + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-cache + + + + org.springframework.boot + spring-boot-starter-test + + + org.mockito + mockito-junit-jupiter + 5.11.0 + test + + + org.springframework.retry + spring-retry + 1.3.1 + + + org.springframework.kafka + spring-kafka + + + org.springframework.kafka + spring-kafka-test + test + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + + --enable-preview + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + --enable-preview + + + + org.springframework.boot + spring-boot-maven-plugin + + --enable-preview + + + + + + \ No newline at end of file diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/YapeTransactionService.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/YapeTransactionService.java new file mode 100644 index 0000000000..a6f58b0f0c --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/YapeTransactionService.java @@ -0,0 +1,13 @@ +package com.tec.yape.transaction; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableAsync; + +@SpringBootApplication +@EnableAsync +public class YapeTransactionService { + public static void main(String[] args) { + SpringApplication.run(YapeTransactionService.class, args); + } +} \ No newline at end of file diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/application/mapper/TransactionUseCaseMapper.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/application/mapper/TransactionUseCaseMapper.java new file mode 100644 index 0000000000..8ac7987b51 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/application/mapper/TransactionUseCaseMapper.java @@ -0,0 +1,35 @@ +package com.tec.yape.transaction.application.mapper; + +import com.tec.yape.transaction.domain.model.TransactionRequest; +import com.tec.yape.transaction.domain.model.TransactionResponse; +import com.tec.yape.transaction.domain.model.TransactionStatus; +import com.tec.yape.transaction.domain.model.TransactionType; +import com.tec.yape.transaction.infrastructure.rest.dto.request.TransactionRequestDto; +import com.tec.yape.transaction.infrastructure.rest.dto.response.TransactionResponseDto; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper(componentModel = "spring", unmappedSourcePolicy = ReportingPolicy.IGNORE, + imports = {TransactionStatus.class, TransactionType.class}) +public interface TransactionUseCaseMapper { + + TransactionUseCaseMapper MAPPER = Mappers.getMapper(TransactionUseCaseMapper.class); + + TransactionRequest toTransactionRequest(TransactionRequestDto requestDto); + + + TransactionRequest toTransactionResponse(TransactionResponse transactionResponse); + + + @Mapping( + target = "transactionStatus", + expression = "java(new TransactionStatus(transaction.getTransactionStatus().name()))" + ) + @Mapping( + target = "transactionType", + expression = "java(new TransactionType(\"TRANSFER\"))" + ) + TransactionResponseDto toTransactionResponseDto(TransactionResponse transaction); +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/application/usecase/TransactionUpdateUseCase.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/application/usecase/TransactionUpdateUseCase.java new file mode 100644 index 0000000000..c00c061910 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/application/usecase/TransactionUpdateUseCase.java @@ -0,0 +1,34 @@ +package com.tec.yape.transaction.application.usecase; + +import com.tec.yape.transaction.application.mapper.TransactionUseCaseMapper; +import com.tec.yape.transaction.domain.model.TransactionRequest; +import com.tec.yape.transaction.domain.model.TransactionResponse; +import com.tec.yape.transaction.domain.model.TransactionValidatedEvent; +import com.tec.yape.transaction.domain.port.input.TransactionUpdateInPort; +import com.tec.yape.transaction.domain.port.output.TransactionOutPort; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class TransactionUpdateUseCase implements TransactionUpdateInPort { + + private final TransactionOutPort transactionOutPort; + + @Override + public void updateTransactionStatus(TransactionValidatedEvent event) { + log.info("[TransactionUpdateUseCase] Updating transaction {}", event.transactionExternalId()); + + TransactionResponse responseDto = transactionOutPort.findByExternalId(event.transactionExternalId()); + + TransactionRequest transactionRequest = TransactionUseCaseMapper.MAPPER.toTransactionResponse(responseDto); + + transactionRequest.setTransactionStatus(event.status()); + + transactionOutPort.update(transactionRequest); + + log.info("[TransactionUpdateUseCase] Transaction {} updated to {}", transactionRequest.getTransactionExternalId(), transactionRequest.getTransactionStatus()); + } +} \ No newline at end of file diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/application/usecase/TransactionUseCase.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/application/usecase/TransactionUseCase.java new file mode 100644 index 0000000000..5447ba8bd9 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/application/usecase/TransactionUseCase.java @@ -0,0 +1,73 @@ +package com.tec.yape.transaction.application.usecase; + + +import com.tec.yape.transaction.application.mapper.TransactionUseCaseMapper; +import com.tec.yape.transaction.domain.enums.TransactionStatus; +import com.tec.yape.transaction.domain.model.TransactionRequest; +import com.tec.yape.transaction.domain.model.TransactionCreatedEvent; +import com.tec.yape.transaction.domain.model.TransactionResponse; +import com.tec.yape.transaction.domain.port.output.TransactionEventPublisherOutPort; +import com.tec.yape.transaction.infrastructure.rest.dto.request.TransactionRequestDto; +import com.tec.yape.transaction.infrastructure.rest.dto.response.TransactionResponseDto; +import com.tec.yape.transaction.domain.port.input.TransactionInPort; +import com.tec.yape.transaction.domain.port.output.TransactionOutPort; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +@Slf4j +public class TransactionUseCase implements TransactionInPort { + + private final TransactionOutPort transactionOutPort; + private final TransactionEventPublisherOutPort eventPublisherOutPort; + + @Override + public TransactionResponseDto registerTransaction(TransactionRequestDto request) { + log.info("[TransactionUseCase] start register transaction"); + + TransactionRequest transactionRequest = TransactionUseCaseMapper.MAPPER.toTransactionRequest(request); + transactionRequest.setTransactionExternalId(UUID.randomUUID()); + transactionRequest.setCreatedAt(LocalDateTime.now()); + transactionRequest.setTransactionStatus(TransactionStatus.PENDIENTE); + + TransactionResponse transactionResponse = transactionOutPort.create(transactionRequest); + + TransactionResponseDto response = TransactionUseCaseMapper.MAPPER.toTransactionResponseDto(transactionResponse); + + TransactionCreatedEvent event = new TransactionCreatedEvent( + transactionResponse.getTransactionExternalId(), + transactionResponse.getValue() + ); + + eventPublisherOutPort.publish(event); + log.info("[TransactionUseCase] end register transaction"); + return response; + } + + @Override + public Page findAllPageable(Pageable pageable) { + Page page = transactionOutPort.findAllPageable(pageable); + return page.map(TransactionUseCaseMapper.MAPPER::toTransactionResponseDto); + } + + @Override + public List findAll() { + List transactionResponse = transactionOutPort.findAll(); + return transactionResponse.stream().map(TransactionUseCaseMapper.MAPPER::toTransactionResponseDto).toList(); + } + + @Override + public TransactionResponseDto findByExternalId(UUID transactionExternalId) { + TransactionResponse transactionResponse = transactionOutPort.findByExternalId(transactionExternalId); + return TransactionUseCaseMapper.MAPPER.toTransactionResponseDto(transactionResponse); + } + +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/enums/TransactionStatus.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/enums/TransactionStatus.java new file mode 100644 index 0000000000..2849499d02 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/enums/TransactionStatus.java @@ -0,0 +1,7 @@ +package com.tec.yape.transaction.domain.enums; + +public enum TransactionStatus { + PENDIENTE, + APROBADO, + RECHAZADO +} \ No newline at end of file diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/exception/CommonErrorType.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/exception/CommonErrorType.java new file mode 100644 index 0000000000..f9a0cb9e8a --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/exception/CommonErrorType.java @@ -0,0 +1,20 @@ +package com.tec.yape.transaction.domain.exception; + +import lombok.Getter; + +@Getter +public enum CommonErrorType { + + COMMON_ERROR_400_1("Error in the sorting criteria"), + COMMON_ERROR_400_2("Invalid request"), + COMMON_ERROR_400_3("Invalid format for field"), + + COMMON_ERROR_404_1("Record not found"); + + private final String description; + + + CommonErrorType(String description) { + this.description = description; + } +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/exception/TransactionException.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/exception/TransactionException.java new file mode 100644 index 0000000000..5b32418a55 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/exception/TransactionException.java @@ -0,0 +1,19 @@ +package com.tec.yape.transaction.domain.exception; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.springframework.http.HttpStatus; + +@EqualsAndHashCode(callSuper = true) +@Data +public class TransactionException extends RuntimeException { + + public HttpStatus status; + public String code; + + public TransactionException(HttpStatus httpStatus, String code, String message) { + super(message); + this.status = httpStatus; + this.code = code; + } +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionCreatedEvent.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionCreatedEvent.java new file mode 100644 index 0000000000..20a0e2cb6c --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionCreatedEvent.java @@ -0,0 +1,9 @@ +package com.tec.yape.transaction.domain.model; + +import java.math.BigDecimal; +import java.util.UUID; + +public record TransactionCreatedEvent( + UUID transactionExternalId, + BigDecimal value +) {} \ No newline at end of file diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionRequest.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionRequest.java new file mode 100644 index 0000000000..4dd291d25a --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionRequest.java @@ -0,0 +1,24 @@ +package com.tec.yape.transaction.domain.model; + +import com.tec.yape.transaction.domain.enums.TransactionStatus; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +public class TransactionRequest { + + private UUID id; + private UUID transactionExternalId; + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private Integer transferTypeId; + private BigDecimal value; + private TransactionStatus transactionStatus; + private LocalDateTime createdAt; + +} \ No newline at end of file diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionResponse.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionResponse.java new file mode 100644 index 0000000000..824b60d5b9 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionResponse.java @@ -0,0 +1,24 @@ +package com.tec.yape.transaction.domain.model; + +import com.tec.yape.transaction.domain.enums.TransactionStatus; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +public class TransactionResponse { + + private UUID id; + private UUID transactionExternalId; + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private Integer transferTypeId; + private BigDecimal value; + private TransactionStatus transactionStatus; + private LocalDateTime createdAt; + +} \ No newline at end of file diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionStatus.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionStatus.java new file mode 100644 index 0000000000..dc60bf37dc --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionStatus.java @@ -0,0 +1,5 @@ +package com.tec.yape.transaction.domain.model; + + +public record TransactionStatus(String name) { +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionType.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionType.java new file mode 100644 index 0000000000..08255024c6 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionType.java @@ -0,0 +1,4 @@ +package com.tec.yape.transaction.domain.model; + +public record TransactionType(String name) { +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionValidatedEvent.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionValidatedEvent.java new file mode 100644 index 0000000000..9965e3d70d --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/model/TransactionValidatedEvent.java @@ -0,0 +1,10 @@ +package com.tec.yape.transaction.domain.model; + +import com.tec.yape.transaction.domain.enums.TransactionStatus; + +import java.util.UUID; + +public record TransactionValidatedEvent( + UUID transactionExternalId, + TransactionStatus status) { +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/input/TransactionInPort.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/input/TransactionInPort.java new file mode 100644 index 0000000000..5d68262f30 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/input/TransactionInPort.java @@ -0,0 +1,24 @@ +package com.tec.yape.transaction.domain.port.input; + +import com.tec.yape.transaction.infrastructure.rest.dto.request.TransactionRequestDto; +import com.tec.yape.transaction.infrastructure.rest.dto.response.TransactionResponseDto; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.UUID; + +public interface TransactionInPort { + + TransactionResponseDto registerTransaction(TransactionRequestDto transactionRequestDto); + + Page findAllPageable( + @ParameterObject Pageable pageable); + + List findAll(); + + + TransactionResponseDto findByExternalId(UUID transactionExternalId); + +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/input/TransactionUpdateInPort.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/input/TransactionUpdateInPort.java new file mode 100644 index 0000000000..4aa37bd5f3 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/input/TransactionUpdateInPort.java @@ -0,0 +1,7 @@ +package com.tec.yape.transaction.domain.port.input; + +import com.tec.yape.transaction.domain.model.TransactionValidatedEvent; + +public interface TransactionUpdateInPort { + void updateTransactionStatus(TransactionValidatedEvent event); +} \ No newline at end of file diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/output/TransactionEventPublisherOutPort.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/output/TransactionEventPublisherOutPort.java new file mode 100644 index 0000000000..32c8ae736c --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/output/TransactionEventPublisherOutPort.java @@ -0,0 +1,8 @@ +package com.tec.yape.transaction.domain.port.output; + +import com.tec.yape.transaction.domain.model.TransactionCreatedEvent; + +public interface TransactionEventPublisherOutPort { + + void publish(TransactionCreatedEvent transaction); +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/output/TransactionOutPort.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/output/TransactionOutPort.java new file mode 100644 index 0000000000..f8e4e89fb3 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/port/output/TransactionOutPort.java @@ -0,0 +1,22 @@ +package com.tec.yape.transaction.domain.port.output; + +import com.tec.yape.transaction.domain.model.TransactionRequest; +import com.tec.yape.transaction.domain.model.TransactionResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; +import java.util.UUID; + +public interface TransactionOutPort { + + Page findAllPageable(Pageable pageable); + + List findAll(); + + TransactionResponse create(TransactionRequest transactionRequest); + + void update(TransactionRequest transactionRequest); + + TransactionResponse findByExternalId(UUID transactionExternalId); +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/util/BeanConstants.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/util/BeanConstants.java new file mode 100644 index 0000000000..88818bba51 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/util/BeanConstants.java @@ -0,0 +1,8 @@ +package com.tec.yape.transaction.domain.util; + +public class BeanConstants { + + public static final String ASYNC_SAVE_CALL_HISTORY = "asyncSaveCallHistory"; + + public static final String ASYNC_VIRTUAL_SAVE_CALL_HISTORY = "asyncVirtualSaveCallHistory"; +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/util/JsonUtil.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/util/JsonUtil.java new file mode 100644 index 0000000000..79cb80cdc6 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/domain/util/JsonUtil.java @@ -0,0 +1,40 @@ +package com.tec.yape.transaction.domain.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +public class JsonUtil { + + + private JsonUtil() { + + } + + public static String ToJSON(Object object) throws JsonProcessingException { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.registerModule(new Jdk8Module()); + mapper.registerModule(new JavaTimeModule()); + return mapper.writeValueAsString(object); + } + + public static T fromJson(String message, Class tClass) { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.registerModule(new Jdk8Module()); + mapper.registerModule(new JavaTimeModule()); + + try { + return mapper.readValue(message, tClass); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/adapter/TransactionAdapter.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/adapter/TransactionAdapter.java new file mode 100644 index 0000000000..1951ca3577 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/adapter/TransactionAdapter.java @@ -0,0 +1,74 @@ +package com.tec.yape.transaction.infrastructure.adapter; + + +import com.tec.yape.transaction.domain.exception.CommonErrorType; +import com.tec.yape.transaction.domain.exception.TransactionException; +import com.tec.yape.transaction.domain.model.TransactionRequest; +import com.tec.yape.transaction.domain.model.TransactionResponse; +import com.tec.yape.transaction.domain.port.output.TransactionOutPort; +import com.tec.yape.transaction.infrastructure.helper.TransactionHelper; +import com.tec.yape.transaction.infrastructure.repository.TransactionRepository; +import com.tec.yape.transaction.infrastructure.repository.model.entity.TransactionEntity; +import com.tec.yape.transaction.infrastructure.repository.model.mapper.TransactionMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.UUID; + + +@Component +@RequiredArgsConstructor +public class TransactionAdapter implements TransactionOutPort { + + private final TransactionRepository transactionRepository; + + @Override + public Page findAllPageable(Pageable pageable) { + + Sort sort = pageable.getSort().isUnsorted() ? Sort.by("id") : pageable.getSort(); + if (!TransactionHelper.validateSorName(sort)) { + throw new TransactionException(HttpStatus.BAD_REQUEST, CommonErrorType.COMMON_ERROR_400_1.name(), CommonErrorType.COMMON_ERROR_400_1.getDescription()); + } + + return transactionRepository.findAll(PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), sort)) + .map(TransactionMapper.MAPPER::toTransactionResponse); + } + + @Override + public List findAll() { + return transactionRepository.findAll().stream() + .map(TransactionMapper.MAPPER::toTransactionResponse).toList(); + } + + + @Override + public TransactionResponse create(TransactionRequest transactionRequest) { + TransactionEntity transactionEntity = TransactionMapper.MAPPER.toTransaction(transactionRequest); + return TransactionMapper.MAPPER.toTransactionResponse(transactionRepository.save(transactionEntity)); + } + + @Override + public void update(TransactionRequest transactionRequest) { + TransactionEntity transactionEntity = TransactionMapper.MAPPER.toTransaction(transactionRequest); + if (transactionEntity == null) { + throw new TransactionException(HttpStatus.NOT_FOUND, CommonErrorType.COMMON_ERROR_404_1.name(), CommonErrorType.COMMON_ERROR_404_1.getDescription()); + } + TransactionMapper.MAPPER.toTransactionResponse(transactionRepository.save(transactionEntity)); + } + + @Override + public TransactionResponse findByExternalId(UUID transactionExternalId) { + TransactionEntity transactionEntity = transactionRepository.findByTransactionExternalId(transactionExternalId); + if (transactionEntity == null) { + throw new TransactionException(HttpStatus.NOT_FOUND, CommonErrorType.COMMON_ERROR_404_1.name(), CommonErrorType.COMMON_ERROR_404_1.getDescription()); + } + + return TransactionMapper.MAPPER.toTransactionResponse(transactionEntity); + } +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/adapter/TransactionPublisherAdapter.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/adapter/TransactionPublisherAdapter.java new file mode 100644 index 0000000000..b78116453d --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/adapter/TransactionPublisherAdapter.java @@ -0,0 +1,21 @@ +package com.tec.yape.transaction.infrastructure.adapter; + +import com.tec.yape.transaction.domain.model.TransactionCreatedEvent; +import com.tec.yape.transaction.domain.port.output.TransactionEventPublisherOutPort; +import com.tec.yape.transaction.infrastructure.messaging.producer.TransactionEventPublisher; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RequiredArgsConstructor +public class TransactionPublisherAdapter implements TransactionEventPublisherOutPort { + + private final TransactionEventPublisher transactionEventPublisher; + + @Override + public void publish(TransactionCreatedEvent transaction) { + transactionEventPublisher.publish(transaction); + } +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/ExecutorAsyncVirtualConfig.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/ExecutorAsyncVirtualConfig.java new file mode 100644 index 0000000000..9ef30ef76a --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/ExecutorAsyncVirtualConfig.java @@ -0,0 +1,19 @@ +package com.tec.yape.transaction.infrastructure.config; + +import com.tec.yape.transaction.domain.util.BeanConstants; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +@Configuration +public class ExecutorAsyncVirtualConfig { + + @Bean(name = BeanConstants.ASYNC_VIRTUAL_SAVE_CALL_HISTORY) + public Executor asyncVirtualSaveCallHistory() { + return Executors.newThreadPerTaskExecutor( + Thread.ofVirtual().name("virtual-thread-", 0)::unstarted + ); + } +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/GlobalExceptionHandler.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/GlobalExceptionHandler.java new file mode 100644 index 0000000000..288f7ba024 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/GlobalExceptionHandler.java @@ -0,0 +1,68 @@ +package com.tec.yape.transaction.infrastructure.config; + +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.tec.yape.transaction.domain.exception.CommonErrorType; +import com.tec.yape.transaction.domain.exception.TransactionException; +import com.tec.yape.transaction.infrastructure.config.dto.ErrorDto; +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; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(value = TransactionException.class) + public ResponseEntity businessExceptionHandler(TransactionException ex) { + ErrorDto error = ErrorDto.builder() + .status(ex.getStatus()) + .code(ex.getCode()).message(ex.getMessage()).build(); + return new ResponseEntity<>(error, ex.getStatus()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity validationExceptionHandler( + MethodArgumentNotValidException ex) { + + String message = ex.getBindingResult() + .getFieldErrors() + .stream() + .map(error -> error.getField() + ": " + error.getDefaultMessage()) + .findFirst() + .orElse(CommonErrorType.COMMON_ERROR_400_2.getDescription()); + + ErrorDto error = ErrorDto.builder() + .status(HttpStatus.BAD_REQUEST) + .code( CommonErrorType.COMMON_ERROR_400_1.name()) + .message(message) + .build(); + + return ResponseEntity.badRequest().body(error); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleInvalidFormat(HttpMessageNotReadableException ex) { + + String message = "Malformed JSON request"; + + if (ex.getCause() instanceof InvalidFormatException invalidFormat) { + String fieldName = invalidFormat.getPath().stream() + .map(JsonMappingException.Reference::getFieldName) + .findFirst() + .orElse("unknown"); + + message = String.format(CommonErrorType.COMMON_ERROR_400_3.getDescription()+ " '%s'", fieldName); + } + + ErrorDto error = ErrorDto.builder() + .status(HttpStatus.BAD_REQUEST) + .code(CommonErrorType.COMMON_ERROR_400_3.name()) + .message(message) + .build(); + + return ResponseEntity.badRequest().body(error); + } +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/OpenApicConfig.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/OpenApicConfig.java new file mode 100644 index 0000000000..be3bf00da9 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/OpenApicConfig.java @@ -0,0 +1,35 @@ +package com.tec.yape.transaction.infrastructure.config; + +import io.swagger.v3.oas.annotations.OpenAPIDefinition; +import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; +import io.swagger.v3.oas.annotations.info.Contact; +import io.swagger.v3.oas.annotations.info.Info; +import io.swagger.v3.oas.annotations.info.License; +import io.swagger.v3.oas.annotations.security.SecurityScheme; +import org.springframework.context.annotation.Configuration; + +@Configuration +@OpenAPIDefinition( + info = @Info( + title = "API-REST TRANSACTION", + version = "1.0", + + description = "Api Rest para transacciones", + + license = @License( + name = "Apache 2.0", + url = "http://www.apache.org/licenses/LICENSE-2.0.html" + ), + + contact = @Contact( + name = "evercarlos.com", + url = "evercarlos.com", + email = "evercarlosrojas@gmail.com") + ) +) +@SecurityScheme(name = "bearerAuth", type = SecuritySchemeType.HTTP, bearerFormat = "JWT", description = "Autenticación" + + "tipo Bearer API-TC", scheme = "bearer") +public class OpenApicConfig { + + +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/dto/ErrorDto.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/dto/ErrorDto.java new file mode 100644 index 0000000000..7943aa2499 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/dto/ErrorDto.java @@ -0,0 +1,13 @@ +package com.tec.yape.transaction.infrastructure.config.dto; + +import lombok.Builder; +import lombok.Data; +import org.springframework.http.HttpStatus; + +@Data +@Builder +public class ErrorDto { + private String code; + private String message; + private HttpStatus status; +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/kafka/consumer/KafkaConsumerConfig.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/kafka/consumer/KafkaConsumerConfig.java new file mode 100644 index 0000000000..c4f1a0045c --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/kafka/consumer/KafkaConsumerConfig.java @@ -0,0 +1,44 @@ +package com.tec.yape.transaction.infrastructure.config.kafka.consumer; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.config.KafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.listener.ConcurrentMessageListenerContainer; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaConsumerConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String boostrapServers; + + + public Map consumerConfig() { + Map properties = new HashMap<>(); + properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, boostrapServers); + properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringSerializer.class);// deserializa: convirte de string a bytes + properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringSerializer.class);// deserializa + return properties; + } + + @Bean + public ConsumerFactory consumerFactory() { + return new DefaultKafkaConsumerFactory<>(consumerConfig()); + } + + @Bean // Para poder inyectar en otros lugares + public KafkaListenerContainerFactory> consumer() { + ConcurrentKafkaListenerContainerFactory factory = new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory()); + return factory; + } + +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/kafka/producer/KafkaProducerConfig.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/kafka/producer/KafkaProducerConfig.java new file mode 100644 index 0000000000..c244942525 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/config/kafka/producer/KafkaProducerConfig.java @@ -0,0 +1,41 @@ +package com.tec.yape.transaction.infrastructure.config.kafka.producer; + +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaProducerConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String boostrapServers; + + + public Map producerConfig() { + Map properties = new HashMap<>(); + properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, boostrapServers); + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);// serializa: convirte de string a bytes + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);// serializa + return properties; + } + + @Bean + public ProducerFactory providerFactory() { + return new DefaultKafkaProducerFactory<>(producerConfig()); + } + + // Envian mensaje + @Bean + public KafkaTemplate kafkaTemplate(ProducerFactory providerFactory) {// inyectando + return new KafkaTemplate<>(providerFactory); + } + +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/helper/TransactionHelper.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/helper/TransactionHelper.java new file mode 100644 index 0000000000..64637172a3 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/helper/TransactionHelper.java @@ -0,0 +1,19 @@ +package com.tec.yape.transaction.infrastructure.helper; + +import org.springframework.data.domain.Sort; + +public class TransactionHelper { + + private TransactionHelper() { + + } + + public static boolean validateSorName(Sort sort) { + if (sort.isSorted()) { + return !sort.iterator().next().getProperty().equals("string"); + } else { + System.out.println("No sort criteria applied"); + return true; + } + } +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/messaging/consumer/TransactionValidatedConsumer.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/messaging/consumer/TransactionValidatedConsumer.java new file mode 100644 index 0000000000..a2a59e6392 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/messaging/consumer/TransactionValidatedConsumer.java @@ -0,0 +1,32 @@ +package com.tec.yape.transaction.infrastructure.messaging.consumer; + +import com.tec.yape.transaction.domain.model.TransactionValidatedEvent; +import com.tec.yape.transaction.domain.port.input.TransactionUpdateInPort; +import com.tec.yape.transaction.domain.util.JsonUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.messaging.handler.annotation.Headers; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.util.Map; + +@Component +@Slf4j +@RequiredArgsConstructor +public class TransactionValidatedConsumer { + + private final TransactionUpdateInPort transactionUpdateInPort; + + @KafkaListener(topics = "topic-transaction-validated", groupId = "transaction-service-group") + public void listener(String message, @Headers Map headers) { + log.info("[TransactionValidatedConsumer] Event received: {}", message); + + String jsonMessage = new String(message.getBytes(), StandardCharsets.UTF_8); + + TransactionValidatedEvent event = JsonUtil.fromJson(jsonMessage, TransactionValidatedEvent.class); + + transactionUpdateInPort.updateTransactionStatus(event); + } +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/messaging/producer/TransactionEventPublisher.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/messaging/producer/TransactionEventPublisher.java new file mode 100644 index 0000000000..c85adc3660 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/messaging/producer/TransactionEventPublisher.java @@ -0,0 +1,8 @@ +package com.tec.yape.transaction.infrastructure.messaging.producer; + +import com.tec.yape.transaction.domain.model.TransactionCreatedEvent; + +public interface TransactionEventPublisher { + + void publish(TransactionCreatedEvent message); +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/messaging/producer/TransactionEventPublisherImpl.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/messaging/producer/TransactionEventPublisherImpl.java new file mode 100644 index 0000000000..0258974f91 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/messaging/producer/TransactionEventPublisherImpl.java @@ -0,0 +1,52 @@ +package com.tec.yape.transaction.infrastructure.messaging.producer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.tec.yape.transaction.domain.model.TransactionCreatedEvent; +import com.tec.yape.transaction.domain.util.JsonUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.KafkaHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +@Slf4j +public class TransactionEventPublisherImpl implements TransactionEventPublisher { + + private final KafkaTemplate kafkaTemplate; + + public TransactionEventPublisherImpl(KafkaTemplate kafkaTemplate) { + this.kafkaTemplate = kafkaTemplate; + } + + @Override + public void publish(TransactionCreatedEvent event) { + log.info("[TransactionEventPublisherImpl] {}", "Start async process"); + + try { + String message = JsonUtil.ToJSON(event); + + Message kafkaMessage = MessageBuilder + .withPayload(message) + .setHeader(KafkaHeaders.TOPIC, "topic-transaction") + .copyHeaders(getHeaders()) + .build(); + + kafkaTemplate.send(kafkaMessage); + + } catch (JsonProcessingException e) { + log.error("[TransactionEventPublisherImpl] message:{}", e.getMessage()); + } + } + + private Map getHeaders() { + Map headers = new HashMap<>(); + headers.put("client-id", "EVER CARLOS ROJAS"); + return headers; + } + +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/repository/TransactionRepository.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/repository/TransactionRepository.java new file mode 100644 index 0000000000..16358f2754 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/repository/TransactionRepository.java @@ -0,0 +1,14 @@ +package com.tec.yape.transaction.infrastructure.repository; + +import com.tec.yape.transaction.infrastructure.repository.model.entity.TransactionEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.UUID; + +public interface TransactionRepository extends JpaRepository { + + @Query("select t from TransactionEntity t where t.transactionExternalId=:transactionExternalId") + TransactionEntity findByTransactionExternalId(UUID transactionExternalId); +} + diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/repository/model/entity/TransactionEntity.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/repository/model/entity/TransactionEntity.java new file mode 100644 index 0000000000..a0757268c7 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/repository/model/entity/TransactionEntity.java @@ -0,0 +1,36 @@ +package com.tec.yape.transaction.infrastructure.repository.model.entity; + + +import com.tec.yape.transaction.domain.enums.TransactionStatus; +import jakarta.persistence.*; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + + +@Entity +@Data +@Table(name = "transacciones") +public class TransactionEntity { + + @Id + @GeneratedValue + private UUID id; + + private UUID transactionExternalId; + + private UUID accountExternalIdDebit; + + private UUID accountExternalIdCredit; + + private Integer transferTypeId; + + private BigDecimal value; + + @Enumerated(EnumType.STRING) + private TransactionStatus transactionStatus; + + private LocalDateTime createdAt; +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/repository/model/mapper/TransactionMapper.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/repository/model/mapper/TransactionMapper.java new file mode 100644 index 0000000000..bde705dc31 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/repository/model/mapper/TransactionMapper.java @@ -0,0 +1,22 @@ +package com.tec.yape.transaction.infrastructure.repository.model.mapper; + +import com.tec.yape.transaction.domain.model.TransactionRequest; +import com.tec.yape.transaction.domain.model.TransactionResponse; +import com.tec.yape.transaction.domain.model.TransactionStatus; +import com.tec.yape.transaction.domain.model.TransactionType; +import com.tec.yape.transaction.infrastructure.repository.model.entity.TransactionEntity; +import org.mapstruct.Mapper; +import org.mapstruct.ReportingPolicy; +import org.mapstruct.factory.Mappers; + +@Mapper(componentModel = "spring", unmappedSourcePolicy = ReportingPolicy.IGNORE, + imports = {TransactionStatus.class, TransactionType.class}) +public interface TransactionMapper { + + TransactionMapper MAPPER = Mappers.getMapper(TransactionMapper.class); + + TransactionEntity toTransaction(TransactionRequest transactionRequest); + + TransactionResponse toTransactionResponse(TransactionEntity transactionEntity); + +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/controller/IndexController.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/controller/IndexController.java new file mode 100644 index 0000000000..5c0c70a26d --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/controller/IndexController.java @@ -0,0 +1,13 @@ +package com.tec.yape.transaction.infrastructure.rest.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class IndexController { + + @RequestMapping("/") + public String getIndex(){ + return "redirect:swagger-ui.html"; + } +} \ No newline at end of file diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/controller/v1/TransactionController.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/controller/v1/TransactionController.java new file mode 100644 index 0000000000..5519b2dd4e --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/controller/v1/TransactionController.java @@ -0,0 +1,56 @@ +package com.tec.yape.transaction.infrastructure.rest.controller.v1; + +import com.tec.yape.transaction.application.usecase.TransactionUseCase; +import com.tec.yape.transaction.infrastructure.rest.dto.request.TransactionRequestDto; +import com.tec.yape.transaction.infrastructure.rest.dto.response.TransactionResponseDto; +import io.swagger.v3.oas.annotations.Operation; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springdoc.api.annotations.ParameterObject; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + + +@RestController +@RequestMapping(value = "/api/v1/transaction", produces = "application/json") +@CrossOrigin("*") +@RequiredArgsConstructor +public class TransactionController { + + private final TransactionUseCase transactionUseCase; + + + @Operation( + summary = "Register a new transaction", + description = "Creates a transaction and sends it to antifraud validation" + ) + @PostMapping + public TransactionResponseDto registerTransaction( @Valid @RequestBody TransactionRequestDto transactionRequestDto) { + return transactionUseCase.registerTransaction(transactionRequestDto); + } + + @Operation(summary = "List transactions", description = "Method order: \"id,asc\"") + @GetMapping("withPagination") + public Page findAllPageable( + @ParameterObject Pageable pageable) { + return transactionUseCase.findAllPageable(pageable); + } + + @Operation(summary = "List transactions") + @GetMapping() + public List findAll() { + return transactionUseCase.findAll(); + } + + @Operation(summary = "Find transaction by ExternalId") + @GetMapping("/{transactionExternalId}") + public TransactionResponseDto findByExternalId( + @PathVariable UUID transactionExternalId) { + return transactionUseCase.findByExternalId(transactionExternalId); + } + +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/dto/request/TransactionRequestDto.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/dto/request/TransactionRequestDto.java new file mode 100644 index 0000000000..961e818a0d --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/dto/request/TransactionRequestDto.java @@ -0,0 +1,32 @@ +package com.tec.yape.transaction.infrastructure.rest.dto.request; + + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.util.UUID; + +@Getter +@Setter +public class TransactionRequestDto { + + @NotNull(message = "accountExternalIdDebit is required") + private UUID accountExternalIdDebit; + + @NotNull(message = "accountExternalIdCredit is required") + private UUID accountExternalIdCredit; + + @NotNull(message = "transferTypeId is required") + @Schema(description = "Transfer type identifier", example = "1", defaultValue = "1") + @JsonProperty("tranferTypeId") + private Integer transferTypeId; + + @NotNull(message = "value is required") + private BigDecimal value; + + +} diff --git a/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/dto/response/TransactionResponseDto.java b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/dto/response/TransactionResponseDto.java new file mode 100644 index 0000000000..364e5a60e2 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/java/com/tec/yape/transaction/infrastructure/rest/dto/response/TransactionResponseDto.java @@ -0,0 +1,26 @@ +package com.tec.yape.transaction.infrastructure.rest.dto.response; + +import com.tec.yape.transaction.domain.model.TransactionStatus; +import com.tec.yape.transaction.domain.model.TransactionType; +import lombok.Getter; +import lombok.Setter; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.UUID; + +@Setter +@Getter +public class TransactionResponseDto { + + private UUID transactionExternalId; + + private TransactionType transactionType; + + private TransactionStatus transactionStatus; + + private BigDecimal value; + + private LocalDateTime createdAt; + +} diff --git a/yape-financial/yape-transaction/src/main/resources/application.properties b/yape-financial/yape-transaction/src/main/resources/application.properties new file mode 100644 index 0000000000..eed9ce1ef7 --- /dev/null +++ b/yape-financial/yape-transaction/src/main/resources/application.properties @@ -0,0 +1,22 @@ +server.tomcat.uri-encoding=UTF-8 +server.port=8081 + +#Datasource +spring.datasource.url=jdbc:postgresql://192.168.18.179:5432/dbtransactionv1 +#spring.datasource.username=postgres +spring.datasource.username=ever +spring.datasource.password=123 + +spring.datasource.driverClassName=org.postgresql.Driver +spring.jpa.show-sql=false +spring.jpa.hibernate.ddl-auto=none +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.properties.hibernate.current_session_context_class=thread +spring.jpa.properties.hibernate.implicit_naming_strategy=org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl +spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults= false +spring.jpa.properties.hibernate.temp.use_nationalized_character_data= true +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect + +# settings KAFKA +#spring.kafka.bootstrap-servers = localhost:9092 +spring.kafka.bootstrap-servers=kafka:9092 diff --git a/yape-financial/yape-transaction/src/test/java/com/tec/transaction/AbstractContextTest.java b/yape-financial/yape-transaction/src/test/java/com/tec/transaction/AbstractContextTest.java new file mode 100644 index 0000000000..ea50b1382c --- /dev/null +++ b/yape-financial/yape-transaction/src/test/java/com/tec/transaction/AbstractContextTest.java @@ -0,0 +1,51 @@ +package com.tec.transaction; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.nio.file.Files; +import java.nio.file.Paths; + +@ExtendWith(SpringExtension.class) +@TestPropertySource(locations = "classpath:application-test.properties") +public class AbstractContextTest { + + protected static final String DEFAULT_TOKEN = ""; + protected static ObjectMapper objectMapper; + + static { + objectMapper = getObjectMapper(); + } + + + protected static String getJsonFromPath(String pathJson) throws Exception { + return new String(Files.readAllBytes(Paths.get(AbstractContextTest.class.getResource(pathJson).toURI()))); + } + + protected static T convertTo(String path, Class aClass) throws Exception { + String jsonRequest = getJsonFromPath(path); + return objectMapper.readValue(jsonRequest, aClass); + } + + public static String convertToJSONString(Object object) throws JsonProcessingException { + ObjectMapper mapper = getObjectMapper(); + return mapper.writeValueAsString(object); + } + + public static ObjectMapper getObjectMapper() { + ObjectMapper mapper = new ObjectMapper(); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + mapper.registerModule(new Jdk8Module()); + mapper.registerModule(new JavaTimeModule()); + return mapper; + } + +} diff --git a/yape-financial/yape-transaction/src/test/java/com/tec/transaction/application/TransactionUpdateUseCaseTest.java b/yape-financial/yape-transaction/src/test/java/com/tec/transaction/application/TransactionUpdateUseCaseTest.java new file mode 100644 index 0000000000..8645fd3ab9 --- /dev/null +++ b/yape-financial/yape-transaction/src/test/java/com/tec/transaction/application/TransactionUpdateUseCaseTest.java @@ -0,0 +1,62 @@ +package com.tec.transaction.application; + +import com.tec.yape.transaction.application.usecase.TransactionUpdateUseCase; +import com.tec.yape.transaction.domain.enums.TransactionStatus; +import com.tec.yape.transaction.domain.model.TransactionResponse; +import com.tec.yape.transaction.domain.model.TransactionValidatedEvent; +import com.tec.yape.transaction.domain.port.output.TransactionOutPort; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.util.UUID; + +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TransactionUpdateUseCaseTest { + + @Mock + private TransactionOutPort transactionOutPort; + + @InjectMocks + private TransactionUpdateUseCase useCase; + + @Test + void shouldUpdateTransactionStatus() { + + TransactionValidatedEvent event = new TransactionValidatedEvent( + UUID.randomUUID(), + TransactionStatus.APROBADO + ); + when(transactionOutPort.findByExternalId(any())) + .thenReturn(transactionResponse()); + + useCase.updateTransactionStatus(event); + + verify(transactionOutPort).update(argThat(request -> + request.getTransactionStatus() == TransactionStatus.APROBADO + )); + } + + + private TransactionResponse transactionResponse() { + UUID transactionId = UUID.fromString("231111ef-5c32-4294-bf0a-18ff2193fa90"); + UUID debitId = UUID.fromString("550e8400-e29b-41d4-a716-446655440000"); + UUID creditId = UUID.fromString("660e8400-e29b-41d4-a716-446655440111"); + + TransactionResponse transactionResponse = new TransactionResponse(); + transactionResponse.setId(UUID.randomUUID()); + transactionResponse.setTransactionExternalId(transactionId); + transactionResponse.setAccountExternalIdCredit(debitId); + transactionResponse.setAccountExternalIdDebit(creditId); + transactionResponse.setValue(new BigDecimal("500")); + transactionResponse.setTransactionStatus(TransactionStatus.PENDIENTE); + return transactionResponse; + } + + +} diff --git a/yape-financial/yape-transaction/src/test/java/com/tec/transaction/application/TransactionUseCaseTest.java b/yape-financial/yape-transaction/src/test/java/com/tec/transaction/application/TransactionUseCaseTest.java new file mode 100644 index 0000000000..83553159e6 --- /dev/null +++ b/yape-financial/yape-transaction/src/test/java/com/tec/transaction/application/TransactionUseCaseTest.java @@ -0,0 +1,69 @@ +package com.tec.transaction.application; + +import com.tec.yape.transaction.application.mapper.TransactionUseCaseMapper; +import com.tec.yape.transaction.application.usecase.TransactionUseCase; +import com.tec.yape.transaction.domain.enums.TransactionStatus; +import com.tec.yape.transaction.domain.model.TransactionRequest; +import com.tec.yape.transaction.domain.model.TransactionResponse; +import com.tec.yape.transaction.domain.port.output.TransactionEventPublisherOutPort; +import com.tec.yape.transaction.domain.port.output.TransactionOutPort; +import com.tec.yape.transaction.infrastructure.rest.dto.request.TransactionRequestDto; +import com.tec.yape.transaction.infrastructure.rest.dto.response.TransactionResponseDto; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class TransactionUseCaseTest { + + @Mock + private TransactionOutPort transactionOutPort; + + @Mock + private TransactionEventPublisherOutPort eventPublisher; + + @InjectMocks + private TransactionUseCase useCase; + + @Test + void shouldCreateTransactionWithPendingStatus() { + + TransactionRequestDto request = new TransactionRequestDto(); + request.setValue(new BigDecimal("500")); + + + when(transactionOutPort.create(any(TransactionRequest.class))) + .thenReturn(transactionResponse()); + + TransactionResponseDto response = useCase.registerTransaction(request); + + assertEquals(TransactionStatus.PENDIENTE.name(), response.getTransactionStatus().name()); + verify(eventPublisher).publish(any()); + } + + + private TransactionResponse transactionResponse() { + UUID transactionId = UUID.fromString("231111ef-5c32-4294-bf0a-18ff2193fa90"); + UUID debitId = UUID.fromString("550e8400-e29b-41d4-a716-446655440000"); + UUID creditId = UUID.fromString("660e8400-e29b-41d4-a716-446655440111"); + + TransactionResponse transactionResponse = new TransactionResponse(); + transactionResponse.setId(UUID.randomUUID()); + transactionResponse.setTransactionExternalId(transactionId); + transactionResponse.setAccountExternalIdCredit(debitId); + transactionResponse.setAccountExternalIdDebit(creditId); + transactionResponse.setValue(new BigDecimal("500")); + transactionResponse.setTransactionStatus(TransactionStatus.PENDIENTE); + return transactionResponse; + } + + +} diff --git a/yape-financial/yape-transaction/src/test/java/com/tec/transaction/infrastructure/TransactionControllerTest.java b/yape-financial/yape-transaction/src/test/java/com/tec/transaction/infrastructure/TransactionControllerTest.java new file mode 100644 index 0000000000..94c9f7191e --- /dev/null +++ b/yape-financial/yape-transaction/src/test/java/com/tec/transaction/infrastructure/TransactionControllerTest.java @@ -0,0 +1,50 @@ +package com.tec.transaction.infrastructure; + +import com.tec.yape.transaction.YapeTransactionService; +import com.tec.yape.transaction.application.usecase.TransactionUseCase; +import com.tec.yape.transaction.infrastructure.rest.controller.v1.TransactionController; +import com.tec.yape.transaction.infrastructure.rest.dto.response.TransactionResponseDto; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; + +import static org.mockito.ArgumentMatchers.any; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(TransactionController.class) +@ContextConfiguration(classes = YapeTransactionService.class) +class TransactionControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private TransactionUseCase transactionUseCase; + + @Test + void shouldCreateTransactionSuccessfully() throws Exception { + + String requestJson = """ + { + "accountExternalIdDebit": "550e8400-e29b-41d4-a716-446655440000", + "accountExternalIdCredit": "660e8400-e29b-41d4-a716-446655440111", + "tranferTypeId": 1, + "value": 500 + } + """; + + Mockito.when(transactionUseCase.registerTransaction(any())) + .thenReturn(new TransactionResponseDto()); + + mockMvc.perform(post("/api/v1/transaction") + .contentType(MediaType.APPLICATION_JSON) + .content(requestJson)) + .andExpect(status().isOk()); + } +} diff --git a/yape-financial/yape-transaction/src/test/resources/application-test.properties b/yape-financial/yape-transaction/src/test/resources/application-test.properties new file mode 100644 index 0000000000..e69de29bb2