From 917f894cc8bc9ed32eaaab534a244b9eeffea225 Mon Sep 17 00:00:00 2001 From: frankdev7 Date: Sun, 23 Mar 2025 20:17:17 -0500 Subject: [PATCH 1/3] first commit --- .gitignore | 27 +++ LICENSE | 201 ++++++++++++++++++ README.md | 80 +++++++ docker-compose.yml | 28 +++ ms-anti-fraud/pom.xml | 27 +++ .../pe/interbank/AntifraudApplication.java | 10 + ms-transaction/pom.xml | 27 +++ .../pe/interbank/TransactionApplication.java | 10 + pom.xml | 32 +++ 9 files changed, 442 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docker-compose.yml create mode 100644 ms-anti-fraud/pom.xml create mode 100644 ms-anti-fraud/src/main/java/pe/interbank/AntifraudApplication.java create mode 100644 ms-transaction/pom.xml create mode 100644 ms-transaction/src/main/java/pe/interbank/TransactionApplication.java create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a084efd --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +target +*.releaseBackup +release.properties + + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +*.log + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fa60f9 --- /dev/null +++ b/README.md @@ -0,0 +1,80 @@ +# Reto de programación en Java :rocket: + +El reto de código que hemos elaborado nos permitirá conocer tus poderosas habilidades de codificación :smile:. + +Ten en cuenta que la forma correcta de enviar tu resolución es a través de un PR :wink: ... ¡Que te diviertas! + +- [Problema](#problem) +- [Stack Tecnológico a utilizar](#tech_stack) +- [Envía tu código](#send_us_your_challenge) + +# Problema + +Cada vez que se crea una transacción (operación) financiera debe ser validada por nuestro microservicio antifraude (Anti-Fraud) y luego el mismo servicio envía un mensaje para actualizar el estado de la transacción. +Por ahora, solo manejamos tres estados de operación. + +
    +
  1. pendiente
  2. +
  3. aprobado
  4. +
  5. rechazado
  6. +
+ +Toda transacción con un valor mayor a 1000 debe ser rechazada. + +```mermaid + flowchart LR + Transaction -- 1) Guarda operación con estado pendiente --> transactionDatabase[(Database)] + Transaction -- 2) Envía evento de registro de operación --> Anti-Fraud + Anti-Fraud -- 3.a) Envía evento de cambio de estado de operación a aprobado --> Transaction + Anti-Fraud -- 3.b) Envía evento de cambio de estado de operación a rechazado --> Transaction + Transaction -- 4) Actualiza operación con el estado recibido por Anti-Fraud --> transactionDatabase[(Database)] +``` + +# Stack Tecnológico a utilizar + +
    +
  1. Java - Spring Framework - Spring Boot
  2. +
  3. BD PostgreSQL
  4. +
  5. Gestor de colas Kafka
  6. +
+ +Nosotros estamos brindandote un `Dockerfile` para que rápidamente puedas configurar tu entorno de desarrollo. + +Deberías tener 2 recursos (a nivel de API): + +1. Recurso para crear una operación que contenga la siguiente estructura: + +```json +{ + "accountExternalIdDebit": "Guid", + "accountExternalIdCredit": "Guid", + "tranferTypeId": 1, + "value": 120 +} +``` + +2. Recurso que recupera una operación + +```json +{ + "transactionExternalId": "Guid", + "transactionType": { + "name": "" + }, + "transactionStatus": { + "name": "" + }, + "value": 120, + "createdAt": "Date" +} +``` + +## Opcional + +Puede utilizar cualquier método para almacenar datos de transacciones, pero debe considerar que podemos lidiar con escenarios de gran volumen en los que tenemos una gran cantidad de escrituras y lecturas de los mismos datos al mismo tiempo. ¿Cómo abordaría este requisito? + +# Envía tu código + +Cuando termines tu resolución, después de realizar el fork al repositorio, tú **debes** abrir una solicitud de extracción (PR) a nuestro repositorio. No hay limitaciones para la implementación, puede seguir el paradigma de programación, la modularización y el estilo que creas que es la solución más adecuada. + +Si tienes alguna duda, por favor ponte en contacto con nosotros. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a59bedf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,28 @@ +version: "3.7" +services: + postgres: + image: postgres:14 + ports: + - "5432:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + zookeeper: + image: confluentinc/cp-zookeeper:5.5.3 + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + kafka: + image: confluentinc/cp-enterprise-kafka:5.5.3 + depends_on: [zookeeper] + 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_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_JMX_PORT: 9991 + ports: + - 9092:9092 +volumes: + oracle-data: + oracle-backup: diff --git a/ms-anti-fraud/pom.xml b/ms-anti-fraud/pom.xml new file mode 100644 index 0000000..24017f5 --- /dev/null +++ b/ms-anti-fraud/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + pe.interbank + java-code-challenge + 1.0-SNAPSHOT + + + ms-anti-fraud + + + + org.springframework.boot + spring-boot-starter-web + + + + + 17 + 17 + UTF-8 + + + \ No newline at end of file diff --git a/ms-anti-fraud/src/main/java/pe/interbank/AntifraudApplication.java b/ms-anti-fraud/src/main/java/pe/interbank/AntifraudApplication.java new file mode 100644 index 0000000..0b02c1f --- /dev/null +++ b/ms-anti-fraud/src/main/java/pe/interbank/AntifraudApplication.java @@ -0,0 +1,10 @@ +package pe.interbank; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AntifraudApplication { + public static void main(String[] args) { + System.out.println("Hello world!"); + } +} \ No newline at end of file diff --git a/ms-transaction/pom.xml b/ms-transaction/pom.xml new file mode 100644 index 0000000..73f012d --- /dev/null +++ b/ms-transaction/pom.xml @@ -0,0 +1,27 @@ + + + 4.0.0 + + pe.interbank + java-code-challenge + 1.0-SNAPSHOT + + + ms-transaction + + + + org.springframework.boot + spring-boot-starter-web + + + + + 17 + 17 + UTF-8 + + + \ No newline at end of file diff --git a/ms-transaction/src/main/java/pe/interbank/TransactionApplication.java b/ms-transaction/src/main/java/pe/interbank/TransactionApplication.java new file mode 100644 index 0000000..eaba67a --- /dev/null +++ b/ms-transaction/src/main/java/pe/interbank/TransactionApplication.java @@ -0,0 +1,10 @@ +package pe.interbank; + +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TransactionApplication { + public static void main(String[] args) { + System.out.println("Hello world!"); + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f34c6a7 --- /dev/null +++ b/pom.xml @@ -0,0 +1,32 @@ + + + 4.0.0 + + pe.interbank + java-code-challenge + 1.0-SNAPSHOT + pom + + ms-anti-fraud + ms-transaction + + + + + + org.springframework.boot + spring-boot-starter-web + 3.4.4 + + + + + + 17 + 17 + UTF-8 + + + \ No newline at end of file From 4b8b366fd1e9a178d96524b51956daf81b3b092d Mon Sep 17 00:00:00 2001 From: frankdev7 Date: Mon, 24 Mar 2025 11:47:15 -0500 Subject: [PATCH 2/3] controller, services, repositories and kafka --- ms-anti-fraud/pom.xml | 30 ++++++- .../pe/interbank/AntifraudApplication.java | 3 +- .../java/pe/interbank/config/KafkaConfig.java | 35 ++++++++ .../kafka/ValidateTransactionConsumer.java | 33 ++++++++ .../java/pe/interbank/model/Transaction.java | 16 ++++ .../interbank/service/AntiFraudService.java | 8 ++ .../service/impl/AntiFraudServiceImpl.java | 48 +++++++++++ .../java/pe/interbank/util/JsonUtils.java | 30 +++++++ .../src/main/resources/application.yml | 8 ++ ms-transaction/pom.xml | 46 +++++++++- .../pe/interbank/TransactionApplication.java | 3 +- .../java/pe/interbank/config/KafkaConfig.java | 35 ++++++++ .../controller/TransactionController.java | 31 +++++++ .../interbank/kafka/TransactionConsumer.java | 32 +++++++ .../model/DTO/TransactionRequest.java | 15 ++++ .../java/pe/interbank/model/Transaction.java | 35 ++++++++ .../repository/TransactionRepository.java | 9 ++ .../interbank/service/TransactionService.java | 14 ++++ .../service/impl/TransactionServiceImpl.java | 84 +++++++++++++++++++ .../java/pe/interbank/util/JsonUtils.java | 30 +++++++ .../src/main/resources/application.yml | 19 +++++ pom.xml | 46 +++++++++- utils/pom.xml | 20 +++++ .../main/java/pe/interbank/util/Contants.java | 10 +++ .../pe/interbank/util/StatusTransaction.java | 17 ++++ .../pe/interbank/util/TypeTransaction.java | 22 +++++ 26 files changed, 673 insertions(+), 6 deletions(-) create mode 100644 ms-anti-fraud/src/main/java/pe/interbank/config/KafkaConfig.java create mode 100644 ms-anti-fraud/src/main/java/pe/interbank/kafka/ValidateTransactionConsumer.java create mode 100644 ms-anti-fraud/src/main/java/pe/interbank/model/Transaction.java create mode 100644 ms-anti-fraud/src/main/java/pe/interbank/service/AntiFraudService.java create mode 100644 ms-anti-fraud/src/main/java/pe/interbank/service/impl/AntiFraudServiceImpl.java create mode 100644 ms-anti-fraud/src/main/java/pe/interbank/util/JsonUtils.java create mode 100644 ms-anti-fraud/src/main/resources/application.yml create mode 100644 ms-transaction/src/main/java/pe/interbank/config/KafkaConfig.java create mode 100644 ms-transaction/src/main/java/pe/interbank/controller/TransactionController.java create mode 100644 ms-transaction/src/main/java/pe/interbank/kafka/TransactionConsumer.java create mode 100644 ms-transaction/src/main/java/pe/interbank/model/DTO/TransactionRequest.java create mode 100644 ms-transaction/src/main/java/pe/interbank/model/Transaction.java create mode 100644 ms-transaction/src/main/java/pe/interbank/repository/TransactionRepository.java create mode 100644 ms-transaction/src/main/java/pe/interbank/service/TransactionService.java create mode 100644 ms-transaction/src/main/java/pe/interbank/service/impl/TransactionServiceImpl.java create mode 100644 ms-transaction/src/main/java/pe/interbank/util/JsonUtils.java create mode 100644 ms-transaction/src/main/resources/application.yml create mode 100644 utils/pom.xml create mode 100644 utils/src/main/java/pe/interbank/util/Contants.java create mode 100644 utils/src/main/java/pe/interbank/util/StatusTransaction.java create mode 100644 utils/src/main/java/pe/interbank/util/TypeTransaction.java diff --git a/ms-anti-fraud/pom.xml b/ms-anti-fraud/pom.xml index 24017f5..a2b10e3 100644 --- a/ms-anti-fraud/pom.xml +++ b/ms-anti-fraud/pom.xml @@ -14,7 +14,35 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webflux + 3.4.3 + + + org.projectlombok + lombok + 1.18.36 + provided + + + com.fasterxml.jackson.core + jackson-databind + 2.18.3 + + + org.springframework.kafka + spring-kafka + 3.3.3 + + + org.slf4j + slf4j-api + 2.0.16 + + + pe.interbank + utils + 1.0-SNAPSHOT + compile diff --git a/ms-anti-fraud/src/main/java/pe/interbank/AntifraudApplication.java b/ms-anti-fraud/src/main/java/pe/interbank/AntifraudApplication.java index 0b02c1f..8100f99 100644 --- a/ms-anti-fraud/src/main/java/pe/interbank/AntifraudApplication.java +++ b/ms-anti-fraud/src/main/java/pe/interbank/AntifraudApplication.java @@ -1,10 +1,11 @@ package pe.interbank; +import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class AntifraudApplication { public static void main(String[] args) { - System.out.println("Hello world!"); + SpringApplication.run(AntifraudApplication.class, args); } } \ No newline at end of file diff --git a/ms-anti-fraud/src/main/java/pe/interbank/config/KafkaConfig.java b/ms-anti-fraud/src/main/java/pe/interbank/config/KafkaConfig.java new file mode 100644 index 0000000..0d88e58 --- /dev/null +++ b/ms-anti-fraud/src/main/java/pe/interbank/config/KafkaConfig.java @@ -0,0 +1,35 @@ +package pe.interbank.config; + +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 KafkaConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Bean + public ProducerFactory producerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } + +} diff --git a/ms-anti-fraud/src/main/java/pe/interbank/kafka/ValidateTransactionConsumer.java b/ms-anti-fraud/src/main/java/pe/interbank/kafka/ValidateTransactionConsumer.java new file mode 100644 index 0000000..2f5cf00 --- /dev/null +++ b/ms-anti-fraud/src/main/java/pe/interbank/kafka/ValidateTransactionConsumer.java @@ -0,0 +1,33 @@ +package pe.interbank.kafka; + +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.KafkaListener; +import pe.interbank.model.Transaction; +import pe.interbank.service.AntiFraudService; +import pe.interbank.util.JsonUtils; + +import static pe.interbank.util.Contants.GROUP_ID_INTERBANK; +import static pe.interbank.util.Contants.TOPIC_TRANSACTION_CREATED; + +@Configuration +public class ValidateTransactionConsumer { + + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(ValidateTransactionConsumer.class); + + @Autowired + private AntiFraudService antiFraudService; + + private final JsonUtils jsonUtils; + + public ValidateTransactionConsumer(JsonUtils jsonUtils) { + this.jsonUtils = jsonUtils; + } + + @KafkaListener(topics = TOPIC_TRANSACTION_CREATED, groupId = GROUP_ID_INTERBANK) + public void consumeTransaction(String message) { + logger.info("Consumed message: {}", message); + antiFraudService.validateTransaction(jsonUtils.fromJson(message, Transaction.class)).subscribe(); + } +} diff --git a/ms-anti-fraud/src/main/java/pe/interbank/model/Transaction.java b/ms-anti-fraud/src/main/java/pe/interbank/model/Transaction.java new file mode 100644 index 0000000..bfa130d --- /dev/null +++ b/ms-anti-fraud/src/main/java/pe/interbank/model/Transaction.java @@ -0,0 +1,16 @@ +package pe.interbank.model; + +import lombok.Data; + +import java.util.Date; + +@Data +public class Transaction { + + private String accountExternalId; + private int tranferTypeId; + private Double value; + private String status; + private Date createdAt; + private Date updatedAt; +} diff --git a/ms-anti-fraud/src/main/java/pe/interbank/service/AntiFraudService.java b/ms-anti-fraud/src/main/java/pe/interbank/service/AntiFraudService.java new file mode 100644 index 0000000..e2ba5b5 --- /dev/null +++ b/ms-anti-fraud/src/main/java/pe/interbank/service/AntiFraudService.java @@ -0,0 +1,8 @@ +package pe.interbank.service; + +import pe.interbank.model.Transaction; +import reactor.core.publisher.Mono; + +public interface AntiFraudService { + Mono validateTransaction(Transaction transaction); +} diff --git a/ms-anti-fraud/src/main/java/pe/interbank/service/impl/AntiFraudServiceImpl.java b/ms-anti-fraud/src/main/java/pe/interbank/service/impl/AntiFraudServiceImpl.java new file mode 100644 index 0000000..dec4aa0 --- /dev/null +++ b/ms-anti-fraud/src/main/java/pe/interbank/service/impl/AntiFraudServiceImpl.java @@ -0,0 +1,48 @@ +package pe.interbank.service.impl; + +import org.slf4j.Logger; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import pe.interbank.model.Transaction; +import pe.interbank.service.AntiFraudService; +import pe.interbank.util.JsonUtils; +import pe.interbank.util.StatusTransaction; +import reactor.core.publisher.Mono; + +import static pe.interbank.util.Contants.MAX_AMOUNT; +import static pe.interbank.util.Contants.TOPIC_TRANSACTION_VALIDATED; + +@Service +public class AntiFraudServiceImpl implements AntiFraudService { + + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(AntiFraudServiceImpl.class); + + private final KafkaTemplate kafkaTemplate; + private final JsonUtils jsonUtils; + + public AntiFraudServiceImpl(KafkaTemplate kafkaTemplate, JsonUtils jsonUtils) { + this.kafkaTemplate = kafkaTemplate; + this.jsonUtils = jsonUtils; + } + + @Override + public Mono validateTransaction(Transaction transaction) { + logger.info("Validating transaction: {}", transaction); + + updateTransactionStatus(transaction); + + String transactionValidated = jsonUtils.toJson(transaction); + + return Mono.fromFuture(kafkaTemplate.send(TOPIC_TRANSACTION_VALIDATED, transactionValidated)) + .doOnSuccess(s -> logger.info("Transaction VALIDATED sent to kafka: {}", transactionValidated)) + .then(); + } + + private void updateTransactionStatus(Transaction transaction) { + if (transaction.getValue() > MAX_AMOUNT) { + transaction.setStatus(StatusTransaction.RECHAZADO.getDescription()); + } else { + transaction.setStatus(StatusTransaction.APROBADO.getDescription()); + } + } +} diff --git a/ms-anti-fraud/src/main/java/pe/interbank/util/JsonUtils.java b/ms-anti-fraud/src/main/java/pe/interbank/util/JsonUtils.java new file mode 100644 index 0000000..45f8a35 --- /dev/null +++ b/ms-anti-fraud/src/main/java/pe/interbank/util/JsonUtils.java @@ -0,0 +1,30 @@ +package pe.interbank.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Component; + +@Component +public class JsonUtils { + private final ObjectMapper objectMapper; + + public JsonUtils(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public String toJson(Object object) { + try { + return objectMapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new RuntimeException("Error converting object to JSON", e); + } + } + + public T fromJson(String json, Class clazz) { + try { + return objectMapper.readValue(json, clazz); + } catch (Exception e) { + throw new RuntimeException("Error parsing JSON", e); + } + } +} diff --git a/ms-anti-fraud/src/main/resources/application.yml b/ms-anti-fraud/src/main/resources/application.yml new file mode 100644 index 0000000..a077fae --- /dev/null +++ b/ms-anti-fraud/src/main/resources/application.yml @@ -0,0 +1,8 @@ +server: + port: 7070 +# Kafka +spring: + kafka: + bootstrap-servers: localhost:9092 + producer: + retries: 1 \ No newline at end of file diff --git a/ms-transaction/pom.xml b/ms-transaction/pom.xml index 73f012d..1233182 100644 --- a/ms-transaction/pom.xml +++ b/ms-transaction/pom.xml @@ -14,10 +14,54 @@ org.springframework.boot - spring-boot-starter-web + spring-boot-starter-webflux + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.postgresql + postgresql + + + org.springframework.boot + spring-boot-starter-reactor-netty + + + io.projectreactor.netty + reactor-netty-http + + + org.projectlombok + lombok + provided + + + com.fasterxml.jackson.core + jackson-databind + + + org.springframework.kafka + spring-kafka + + + org.slf4j + slf4j-api + + + pe.interbank + utils + 1.0-SNAPSHOT + compile + + + + + + 17 17 diff --git a/ms-transaction/src/main/java/pe/interbank/TransactionApplication.java b/ms-transaction/src/main/java/pe/interbank/TransactionApplication.java index eaba67a..84b8d70 100644 --- a/ms-transaction/src/main/java/pe/interbank/TransactionApplication.java +++ b/ms-transaction/src/main/java/pe/interbank/TransactionApplication.java @@ -1,10 +1,11 @@ package pe.interbank; +import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class TransactionApplication { public static void main(String[] args) { - System.out.println("Hello world!"); + SpringApplication.run(TransactionApplication.class, args); } } \ No newline at end of file diff --git a/ms-transaction/src/main/java/pe/interbank/config/KafkaConfig.java b/ms-transaction/src/main/java/pe/interbank/config/KafkaConfig.java new file mode 100644 index 0000000..0d88e58 --- /dev/null +++ b/ms-transaction/src/main/java/pe/interbank/config/KafkaConfig.java @@ -0,0 +1,35 @@ +package pe.interbank.config; + +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 KafkaConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Bean + public ProducerFactory producerFactory() { + Map configProps = new HashMap<>(); + configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return new DefaultKafkaProducerFactory<>(configProps); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } + +} diff --git a/ms-transaction/src/main/java/pe/interbank/controller/TransactionController.java b/ms-transaction/src/main/java/pe/interbank/controller/TransactionController.java new file mode 100644 index 0000000..d15b7cc --- /dev/null +++ b/ms-transaction/src/main/java/pe/interbank/controller/TransactionController.java @@ -0,0 +1,31 @@ +package pe.interbank.controller; + +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; +import pe.interbank.model.DTO.TransactionRequest; +import pe.interbank.model.Transaction; +import pe.interbank.service.TransactionService; +import pe.interbank.service.impl.TransactionServiceImpl; +import reactor.core.publisher.Mono; + +@RestController +@RequestMapping("/transaction") +public class TransactionController { + + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(TransactionController.class); + + @Autowired + TransactionService transactionService; + + @GetMapping("/{accountExternalId}") + public Mono getTransaction(@PathVariable("accountExternalId") String accountExternalId) { + return transactionService.getTransaction(accountExternalId); + } + + @PostMapping + public Mono sendTransaction(@RequestBody TransactionRequest transactionRq) { + return transactionService.sendTransaction(transactionRq); + } + +} diff --git a/ms-transaction/src/main/java/pe/interbank/kafka/TransactionConsumer.java b/ms-transaction/src/main/java/pe/interbank/kafka/TransactionConsumer.java new file mode 100644 index 0000000..be33adf --- /dev/null +++ b/ms-transaction/src/main/java/pe/interbank/kafka/TransactionConsumer.java @@ -0,0 +1,32 @@ +package pe.interbank.kafka; + +import org.slf4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.KafkaListener; +import pe.interbank.model.Transaction; +import pe.interbank.service.TransactionService; +import pe.interbank.util.JsonUtils; + +import static pe.interbank.util.Contants.*; + +@Configuration +public class TransactionConsumer { + + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(TransactionConsumer.class); + + @Autowired + private TransactionService transactionService; + + private final JsonUtils jsonUtils; + + public TransactionConsumer(JsonUtils jsonUtils) { + this.jsonUtils = jsonUtils; + } + + @KafkaListener(topics = TOPIC_TRANSACTION_VALIDATED, groupId = GROUP_ID_INTERBANK) + public void consumeTransaction(String message) { + logger.info("Topic {}: Consumed message: {}", TOPIC_TRANSACTION_VALIDATED, message); + transactionService.updateTransactionStatus(jsonUtils.fromJson(message, Transaction.class)).subscribe(); + } +} diff --git a/ms-transaction/src/main/java/pe/interbank/model/DTO/TransactionRequest.java b/ms-transaction/src/main/java/pe/interbank/model/DTO/TransactionRequest.java new file mode 100644 index 0000000..2ef7a91 --- /dev/null +++ b/ms-transaction/src/main/java/pe/interbank/model/DTO/TransactionRequest.java @@ -0,0 +1,15 @@ +package pe.interbank.model.DTO; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TransactionRequest { + private int tranferTypeId; + private Double value; +} diff --git a/ms-transaction/src/main/java/pe/interbank/model/Transaction.java b/ms-transaction/src/main/java/pe/interbank/model/Transaction.java new file mode 100644 index 0000000..a331a50 --- /dev/null +++ b/ms-transaction/src/main/java/pe/interbank/model/Transaction.java @@ -0,0 +1,35 @@ +package pe.interbank.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + + +@Entity +@Table(name = "transaction") +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class Transaction { + + @Id + private String accountExternalId; + + private int tranferTypeId; + + private Double value; + + private String status; + + private Date createdAt; + + private Date updatedAt; +} diff --git a/ms-transaction/src/main/java/pe/interbank/repository/TransactionRepository.java b/ms-transaction/src/main/java/pe/interbank/repository/TransactionRepository.java new file mode 100644 index 0000000..6e590b0 --- /dev/null +++ b/ms-transaction/src/main/java/pe/interbank/repository/TransactionRepository.java @@ -0,0 +1,9 @@ +package pe.interbank.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import pe.interbank.model.Transaction; + +@Repository +public interface TransactionRepository extends JpaRepository { +} diff --git a/ms-transaction/src/main/java/pe/interbank/service/TransactionService.java b/ms-transaction/src/main/java/pe/interbank/service/TransactionService.java new file mode 100644 index 0000000..59bf9c4 --- /dev/null +++ b/ms-transaction/src/main/java/pe/interbank/service/TransactionService.java @@ -0,0 +1,14 @@ +package pe.interbank.service; + +import pe.interbank.model.DTO.TransactionRequest; +import pe.interbank.model.Transaction; +import reactor.core.publisher.Mono; + +public interface TransactionService { + + Mono getTransaction(String accountExternalId); + + Mono sendTransaction(TransactionRequest transactionRq); + + Mono updateTransactionStatus(Transaction transaction); +} diff --git a/ms-transaction/src/main/java/pe/interbank/service/impl/TransactionServiceImpl.java b/ms-transaction/src/main/java/pe/interbank/service/impl/TransactionServiceImpl.java new file mode 100644 index 0000000..e4f6b78 --- /dev/null +++ b/ms-transaction/src/main/java/pe/interbank/service/impl/TransactionServiceImpl.java @@ -0,0 +1,84 @@ +package pe.interbank.service.impl; + +import org.slf4j.Logger; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; +import pe.interbank.model.DTO.TransactionRequest; +import pe.interbank.model.Transaction; +import pe.interbank.repository.TransactionRepository; +import pe.interbank.service.TransactionService; +import pe.interbank.util.JsonUtils; +import pe.interbank.util.StatusTransaction; +import reactor.core.publisher.Mono; + +import java.util.Date; +import java.util.Optional; +import java.util.UUID; + +import static pe.interbank.util.Contants.TOPIC_TRANSACTION_CREATED; + +@Service +public class TransactionServiceImpl implements TransactionService { + + private static final Logger logger = org.slf4j.LoggerFactory.getLogger(TransactionServiceImpl.class); + + private final TransactionRepository transactionRepository; + + private final KafkaTemplate kafkaTemplate; + private final JsonUtils jsonUtils; + + public TransactionServiceImpl(TransactionRepository transactionRepository, KafkaTemplate kafkaTemplate, JsonUtils jsonUtils) { + this.transactionRepository = transactionRepository; + this.kafkaTemplate = kafkaTemplate; + this.jsonUtils = jsonUtils; + } + + @Override + public Mono getTransaction(String accountExternalId) { + Optional transaction = transactionRepository.findById(accountExternalId); + + if (!transaction.isPresent()) { + logger.error("Transaction not found: {}", accountExternalId); + return Mono.empty(); + } + + return Mono.just(transaction.get()); + } + + @Override + public Mono sendTransaction(TransactionRequest transactionRq) { + Transaction transaction = Transaction.builder() + .accountExternalId(UUID.randomUUID().toString()) + .tranferTypeId(transactionRq.getTranferTypeId()) + .value(transactionRq.getValue()) + .status(StatusTransaction.PENDIENTE.getDescription()) + .createdAt(new Date()) + .build(); + + Transaction transactionSaved = transactionRepository.save(transaction); + logger.info("Transaction created: {}", transactionSaved); + + String transactionJson = jsonUtils.toJson(transactionSaved); + kafkaTemplate.send(TOPIC_TRANSACTION_CREATED, transactionJson); + logger.info("Transaction PENDIENTE sent to kafka: {}", transactionJson); + + return Mono.just(transactionSaved); + } + + @Override + public Mono updateTransactionStatus(Transaction transactionRq) { + + Optional transaction = transactionRepository.findById(transactionRq.getAccountExternalId()); + + if (transaction.isPresent()) { + Transaction transactionToUpdate = transaction.get(); + transactionToUpdate.setStatus(transactionRq.getStatus()); + transactionToUpdate.setUpdatedAt(new Date()); + transactionRepository.save(transactionToUpdate); + logger.info("Transaction updated: {}", transactionToUpdate); + } + + return Mono.empty(); + } + +} diff --git a/ms-transaction/src/main/java/pe/interbank/util/JsonUtils.java b/ms-transaction/src/main/java/pe/interbank/util/JsonUtils.java new file mode 100644 index 0000000..45f8a35 --- /dev/null +++ b/ms-transaction/src/main/java/pe/interbank/util/JsonUtils.java @@ -0,0 +1,30 @@ +package pe.interbank.util; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Component; + +@Component +public class JsonUtils { + private final ObjectMapper objectMapper; + + public JsonUtils(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public String toJson(Object object) { + try { + return objectMapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new RuntimeException("Error converting object to JSON", e); + } + } + + public T fromJson(String json, Class clazz) { + try { + return objectMapper.readValue(json, clazz); + } catch (Exception e) { + throw new RuntimeException("Error parsing JSON", e); + } + } +} diff --git a/ms-transaction/src/main/resources/application.yml b/ms-transaction/src/main/resources/application.yml new file mode 100644 index 0000000..33f7957 --- /dev/null +++ b/ms-transaction/src/main/resources/application.yml @@ -0,0 +1,19 @@ +server: + port: 8080 +spring: + datasource: + url: jdbc:postgresql://localhost:5432/db_fraud + username: postgres + password: postgres + driver-class-name: org.postgresql.Driver + jpa: + database: POSTGRESQL + generate-ddl: true + database-platform: org.hibernate.dialect.PostgreSQLDialect + + # Kafka + kafka: + bootstrap-servers: localhost:9092 + producer: + retries: 1 + diff --git a/pom.xml b/pom.xml index f34c6a7..141ac02 100644 --- a/pom.xml +++ b/pom.xml @@ -11,14 +11,56 @@ ms-anti-fraud ms-transaction + utils org.springframework.boot - spring-boot-starter-web - 3.4.4 + spring-boot-starter-webflux + 3.4.3 + + + org.springframework.boot + spring-boot-starter-data-jpa + 3.4.3 + + + org.postgresql + postgresql + 42.7.5 + + + org.springframework.boot + spring-boot-starter-reactor-netty + 3.4.3 + + + io.projectreactor.netty + reactor-netty-http + 1.2.1 + + + org.projectlombok + lombok + 1.18.36 + provided + + + com.fasterxml.jackson.core + jackson-databind + 2.18.3 + + + org.springframework.kafka + spring-kafka + 3.3.3 + + + org.slf4j + slf4j-api + 2.0.16 diff --git a/utils/pom.xml b/utils/pom.xml new file mode 100644 index 0000000..2e364ba --- /dev/null +++ b/utils/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + pe.interbank + java-code-challenge + 1.0-SNAPSHOT + + + utils + + + 17 + 17 + UTF-8 + + + \ No newline at end of file diff --git a/utils/src/main/java/pe/interbank/util/Contants.java b/utils/src/main/java/pe/interbank/util/Contants.java new file mode 100644 index 0000000..fad387d --- /dev/null +++ b/utils/src/main/java/pe/interbank/util/Contants.java @@ -0,0 +1,10 @@ +package pe.interbank.util; + +public class Contants { + + public static final String GROUP_ID_INTERBANK = "pe.interbank"; + public static final String TOPIC_TRANSACTION_CREATED = "topic-transaction-created"; + public static final String TOPIC_TRANSACTION_VALIDATED = "topic-transaction-validated"; + + public static final Double MAX_AMOUNT = 1000.0; +} diff --git a/utils/src/main/java/pe/interbank/util/StatusTransaction.java b/utils/src/main/java/pe/interbank/util/StatusTransaction.java new file mode 100644 index 0000000..0e06a4c --- /dev/null +++ b/utils/src/main/java/pe/interbank/util/StatusTransaction.java @@ -0,0 +1,17 @@ +package pe.interbank.util; + +public enum StatusTransaction { + PENDIENTE("PENDIENTE"), + APROBADO("APROBADO"), + RECHAZADO("RECHAZADO"); + + private String description; + + StatusTransaction(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } +} diff --git a/utils/src/main/java/pe/interbank/util/TypeTransaction.java b/utils/src/main/java/pe/interbank/util/TypeTransaction.java new file mode 100644 index 0000000..cfbd895 --- /dev/null +++ b/utils/src/main/java/pe/interbank/util/TypeTransaction.java @@ -0,0 +1,22 @@ +package pe.interbank.util; + +public enum TypeTransaction { + CREDIT(1, "CREDITO"), + DEBIT(2, "DEBITO"); + + private int type; + private String description; + + TypeTransaction(int type, String description) { + this.type = type; + this.description = description; + } + + public int getType() { + return type; + } + + public String getDescription() { + return description; + } +} From 3bc3d3a775bd424c78a85d9beeca2b44f5fd0a61 Mon Sep 17 00:00:00 2001 From: frankdev7 Date: Mon, 24 Mar 2025 12:01:20 -0500 Subject: [PATCH 3/3] comments --- utils/src/main/java/pe/interbank/util/Contants.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/utils/src/main/java/pe/interbank/util/Contants.java b/utils/src/main/java/pe/interbank/util/Contants.java index fad387d..43f3e42 100644 --- a/utils/src/main/java/pe/interbank/util/Contants.java +++ b/utils/src/main/java/pe/interbank/util/Contants.java @@ -2,9 +2,11 @@ public class Contants { + // KAFKA public static final String GROUP_ID_INTERBANK = "pe.interbank"; public static final String TOPIC_TRANSACTION_CREATED = "topic-transaction-created"; public static final String TOPIC_TRANSACTION_VALIDATED = "topic-transaction-validated"; + // AMOUNT public static final Double MAX_AMOUNT = 1000.0; }