diff --git a/antifraud/.classpath b/antifraud/.classpath new file mode 100644 index 0000000000..c8ae32e9a3 --- /dev/null +++ b/antifraud/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/antifraud/.project b/antifraud/.project new file mode 100644 index 0000000000..23f4adfa97 --- /dev/null +++ b/antifraud/.project @@ -0,0 +1,23 @@ + + + antifraud + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/antifraud/.settings/org.eclipse.core.resources.prefs b/antifraud/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000..abdea9ac03 --- /dev/null +++ b/antifraud/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding/=UTF-8 diff --git a/antifraud/.settings/org.eclipse.jdt.core.prefs b/antifraud/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..5b8827bcae --- /dev/null +++ b/antifraud/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=16 +org.eclipse.jdt.core.compiler.compliance=16 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=16 diff --git a/antifraud/Dockerfile b/antifraud/Dockerfile new file mode 100644 index 0000000000..31d100bac5 --- /dev/null +++ b/antifraud/Dockerfile @@ -0,0 +1,11 @@ +FROM maven:3.9.9-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn -q -DskipTests package + +FROM eclipse-temurin:17-jre +WORKDIR /app +COPY --from=build /app/target/antifraud-service-0.0.1-SNAPSHOT.jar app.jar +EXPOSE 8081 +ENTRYPOINT ["java","-jar","/app/app.jar"] diff --git a/antifraud/pom.xml b/antifraud/pom.xml new file mode 100644 index 0000000000..976430fae8 --- /dev/null +++ b/antifraud/pom.xml @@ -0,0 +1,53 @@ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.7 + + + + com.yape + antifraud-service + 0.0.1-SNAPSHOT + antifraud-service + + + 17 + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.kafka + spring-kafka + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/antifraud/src/main/java/com/yape/antifraud/AntifraudApplication.java b/antifraud/src/main/java/com/yape/antifraud/AntifraudApplication.java new file mode 100644 index 0000000000..a0f21e39f8 --- /dev/null +++ b/antifraud/src/main/java/com/yape/antifraud/AntifraudApplication.java @@ -0,0 +1,11 @@ +package com.yape.antifraud; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class AntifraudApplication { + public static void main(String[] args) { + SpringApplication.run(AntifraudApplication.class, args); + } +} diff --git a/antifraud/src/main/java/com/yape/antifraud/kafka/AntiFraudListener.java b/antifraud/src/main/java/com/yape/antifraud/kafka/AntiFraudListener.java new file mode 100644 index 0000000000..dc460035b0 --- /dev/null +++ b/antifraud/src/main/java/com/yape/antifraud/kafka/AntiFraudListener.java @@ -0,0 +1,28 @@ +package com.yape.antifraud.kafka; + +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; + +@Component +public class AntiFraudListener { + + private final StatusPublisher publisher; + + public AntiFraudListener(StatusPublisher publisher) { + this.publisher = publisher; + } + + @KafkaListener(topics = "${app.topics.created}", groupId = "antifraud-service") + public void onCreated(TransactionCreatedEvent event) { + String status = decide(event.value()); + publisher.publish(new TransactionStatusEvent(event.transactionExternalId(), status)); + } + + private String decide(BigDecimal value) { + if (value == null) return "rejected"; + // Regla del reto: value > 1000 => rejected + return value.compareTo(BigDecimal.valueOf(1000)) > 0 ? "rejected" : "approved"; + } +} diff --git a/antifraud/src/main/java/com/yape/antifraud/kafka/StatusPublisher.java b/antifraud/src/main/java/com/yape/antifraud/kafka/StatusPublisher.java new file mode 100644 index 0000000000..889a71e105 --- /dev/null +++ b/antifraud/src/main/java/com/yape/antifraud/kafka/StatusPublisher.java @@ -0,0 +1,24 @@ +package com.yape.antifraud.kafka; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Component +public class StatusPublisher { + + private final KafkaTemplate kafkaTemplate; + private final String statusTopic; + + public StatusPublisher( + KafkaTemplate kafkaTemplate, + @Value("${app.topics.status}") String statusTopic + ) { + this.kafkaTemplate = kafkaTemplate; + this.statusTopic = statusTopic; + } + + public void publish(TransactionStatusEvent event) { + kafkaTemplate.send(statusTopic, event.transactionExternalId(), event); + } +} diff --git a/antifraud/src/main/java/com/yape/antifraud/kafka/TransactionCreatedEvent.java b/antifraud/src/main/java/com/yape/antifraud/kafka/TransactionCreatedEvent.java new file mode 100644 index 0000000000..ae24dc3b8b --- /dev/null +++ b/antifraud/src/main/java/com/yape/antifraud/kafka/TransactionCreatedEvent.java @@ -0,0 +1,12 @@ +package com.yape.antifraud.kafka; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.math.BigDecimal; + +public record TransactionCreatedEvent( + String transactionExternalId, + String accountExternalIdDebit, + String accountExternalIdCredit, + @JsonProperty("tranferTypeId") Integer tranferTypeId, + BigDecimal value +) {} diff --git a/antifraud/src/main/java/com/yape/antifraud/kafka/TransactionStatusEvent.java b/antifraud/src/main/java/com/yape/antifraud/kafka/TransactionStatusEvent.java new file mode 100644 index 0000000000..a3da17d9a6 --- /dev/null +++ b/antifraud/src/main/java/com/yape/antifraud/kafka/TransactionStatusEvent.java @@ -0,0 +1,6 @@ +package com.yape.antifraud.kafka; + +public record TransactionStatusEvent( + String transactionExternalId, + String status +) {} diff --git a/antifraud/src/main/resources/application.yml b/antifraud/src/main/resources/application.yml new file mode 100644 index 0000000000..e17395abc6 --- /dev/null +++ b/antifraud/src/main/resources/application.yml @@ -0,0 +1,22 @@ +server: + port: 8081 + +spring: + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:29092} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + group-id: antifraud-service + auto-offset-reset: earliest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + spring.json.value.default.type: com.yape.antifraud.kafka.TransactionCreatedEvent + +app: + topics: + created: transactions.created + status: transactions.status diff --git a/antifraud/target/classes/application.yml b/antifraud/target/classes/application.yml new file mode 100644 index 0000000000..e17395abc6 --- /dev/null +++ b/antifraud/target/classes/application.yml @@ -0,0 +1,22 @@ +server: + port: 8081 + +spring: + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:29092} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + group-id: antifraud-service + auto-offset-reset: earliest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + spring.json.value.default.type: com.yape.antifraud.kafka.TransactionCreatedEvent + +app: + topics: + created: transactions.created + status: transactions.status diff --git a/antifraud/target/classes/com/yape/antifraud/AntifraudApplication.class b/antifraud/target/classes/com/yape/antifraud/AntifraudApplication.class new file mode 100644 index 0000000000..436c1593d2 Binary files /dev/null and b/antifraud/target/classes/com/yape/antifraud/AntifraudApplication.class differ diff --git a/antifraud/target/classes/com/yape/antifraud/kafka/AntiFraudListener.class b/antifraud/target/classes/com/yape/antifraud/kafka/AntiFraudListener.class new file mode 100644 index 0000000000..fd3e677d20 Binary files /dev/null and b/antifraud/target/classes/com/yape/antifraud/kafka/AntiFraudListener.class differ diff --git a/antifraud/target/classes/com/yape/antifraud/kafka/StatusPublisher.class b/antifraud/target/classes/com/yape/antifraud/kafka/StatusPublisher.class new file mode 100644 index 0000000000..d5ff2644e5 Binary files /dev/null and b/antifraud/target/classes/com/yape/antifraud/kafka/StatusPublisher.class differ diff --git a/antifraud/target/classes/com/yape/antifraud/kafka/TransactionCreatedEvent.class b/antifraud/target/classes/com/yape/antifraud/kafka/TransactionCreatedEvent.class new file mode 100644 index 0000000000..a6e0508a4b Binary files /dev/null and b/antifraud/target/classes/com/yape/antifraud/kafka/TransactionCreatedEvent.class differ diff --git a/antifraud/target/classes/com/yape/antifraud/kafka/TransactionStatusEvent.class b/antifraud/target/classes/com/yape/antifraud/kafka/TransactionStatusEvent.class new file mode 100644 index 0000000000..f7cda99047 Binary files /dev/null and b/antifraud/target/classes/com/yape/antifraud/kafka/TransactionStatusEvent.class differ diff --git a/transaction/.classpath b/transaction/.classpath new file mode 100644 index 0000000000..c8ae32e9a3 --- /dev/null +++ b/transaction/.classpath @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/transaction/.project b/transaction/.project new file mode 100644 index 0000000000..a8d5578cd2 --- /dev/null +++ b/transaction/.project @@ -0,0 +1,23 @@ + + + transaction + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.m2e.core.maven2Nature + + diff --git a/transaction/.settings/org.eclipse.core.resources.prefs b/transaction/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000000..abdea9ac03 --- /dev/null +++ b/transaction/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,4 @@ +eclipse.preferences.version=1 +encoding//src/main/java=UTF-8 +encoding//src/main/resources=UTF-8 +encoding/=UTF-8 diff --git a/transaction/.settings/org.eclipse.jdt.core.prefs b/transaction/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000000..5b8827bcae --- /dev/null +++ b/transaction/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,9 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.methodParameters=generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=16 +org.eclipse.jdt.core.compiler.compliance=16 +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=16 diff --git a/transaction/Dockerfile b/transaction/Dockerfile new file mode 100644 index 0000000000..e4f29becc8 --- /dev/null +++ b/transaction/Dockerfile @@ -0,0 +1,11 @@ +FROM maven:3.9.9-eclipse-temurin-17 AS build +WORKDIR /app +COPY pom.xml . +COPY src ./src +RUN mvn -q -DskipTests package + +FROM eclipse-temurin:17-jre +WORKDIR /app +COPY --from=build /app/target/transaction-service-0.0.1-SNAPSHOT.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java","-jar","/app/app.jar"] diff --git a/transaction/pom.xml b/transaction/pom.xml new file mode 100644 index 0000000000..2ec9eb64cc --- /dev/null +++ b/transaction/pom.xml @@ -0,0 +1,75 @@ + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.7 + + + + com.yape + transaction-service + 0.0.1-SNAPSHOT + transaction-service + + + 17 + + + + + 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.springframework.kafka + spring-kafka + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.kafka + spring-kafka-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/transaction/src/main/java/com/yape/transactions/TransactionsApplication.java b/transaction/src/main/java/com/yape/transactions/TransactionsApplication.java new file mode 100644 index 0000000000..e44be8ba99 --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/TransactionsApplication.java @@ -0,0 +1,11 @@ +package com.yape.transactions; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TransactionsApplication { + public static void main(String[] args) { + SpringApplication.run(TransactionsApplication.class, args); + } +} diff --git a/transaction/src/main/java/com/yape/transactions/api/ApiExceptionHandler.java b/transaction/src/main/java/com/yape/transactions/api/ApiExceptionHandler.java new file mode 100644 index 0000000000..c6a1836ba7 --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/api/ApiExceptionHandler.java @@ -0,0 +1,34 @@ +package com.yape.transactions.api; + +import com.yape.transactions.service.InvalidUuidException; +import com.yape.transactions.service.TransactionNotFoundException; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Map; + +@RestControllerAdvice +public class ApiExceptionHandler { + + @ExceptionHandler(TransactionNotFoundException.class) + public ResponseEntity> notFound(TransactionNotFoundException ex) { + return ResponseEntity.status(404).body(Map.of("error", ex.getMessage())); + } + + @ExceptionHandler(InvalidUuidException.class) + public ResponseEntity> invalidUuid(InvalidUuidException ex) { + return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage())); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> validation(MethodArgumentNotValidException ex) { + return ResponseEntity.badRequest().body(Map.of( + "error", "Validation error", + "details", ex.getBindingResult().getFieldErrors().stream() + .map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) + .toList() + )); + } +} diff --git a/transaction/src/main/java/com/yape/transactions/api/TransactionController.java b/transaction/src/main/java/com/yape/transactions/api/TransactionController.java new file mode 100644 index 0000000000..86c627ffa5 --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/api/TransactionController.java @@ -0,0 +1,31 @@ +package com.yape.transactions.api; + +import com.yape.transactions.api.dto.CreateTransactionRequest; +import com.yape.transactions.api.dto.CreateTransactionResponse; +import com.yape.transactions.api.dto.TransactionResponse; +import com.yape.transactions.service.TransactionService; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/transactions") +public class TransactionController { + + private final TransactionService transactionService; + + public TransactionController(TransactionService transactionService) { + this.transactionService = transactionService; + } + + @PostMapping + public ResponseEntity create(@Valid @RequestBody CreateTransactionRequest request) { + String externalId = transactionService.createTransaction(request); + return ResponseEntity.status(201).body(new CreateTransactionResponse(externalId)); + } + + @GetMapping("/{transactionExternalId}") + public TransactionResponse get(@PathVariable String transactionExternalId) { + return transactionService.getTransaction(transactionExternalId); + } +} diff --git a/transaction/src/main/java/com/yape/transactions/api/dto/CreateTransactionRequest.java b/transaction/src/main/java/com/yape/transactions/api/dto/CreateTransactionRequest.java new file mode 100644 index 0000000000..42e273ddbf --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/api/dto/CreateTransactionRequest.java @@ -0,0 +1,14 @@ +package com.yape.transactions.api.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.PositiveOrZero; +import java.math.BigDecimal; + +public record CreateTransactionRequest( + @NotBlank String accountExternalIdDebit, + @NotBlank String accountExternalIdCredit, + @JsonProperty("tranferTypeId") @NotNull Integer tranferTypeId, + @NotNull @PositiveOrZero BigDecimal value +) {} diff --git a/transaction/src/main/java/com/yape/transactions/api/dto/CreateTransactionResponse.java b/transaction/src/main/java/com/yape/transactions/api/dto/CreateTransactionResponse.java new file mode 100644 index 0000000000..fc85a68df4 --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/api/dto/CreateTransactionResponse.java @@ -0,0 +1,3 @@ +package com.yape.transactions.api.dto; + +public record CreateTransactionResponse(String transactionExternalId) {} diff --git a/transaction/src/main/java/com/yape/transactions/api/dto/TransactionResponse.java b/transaction/src/main/java/com/yape/transactions/api/dto/TransactionResponse.java new file mode 100644 index 0000000000..a3d423e9ca --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/api/dto/TransactionResponse.java @@ -0,0 +1,14 @@ +package com.yape.transactions.api.dto; + +import java.math.BigDecimal; +import java.time.Instant; + +public record TransactionResponse( + String transactionExternalId, + NamedDto transactionType, + NamedDto transactionStatus, + BigDecimal value, + Instant createdAt +) { + public record NamedDto(String name) {} +} diff --git a/transaction/src/main/java/com/yape/transactions/domain/TransactionEntity.java b/transaction/src/main/java/com/yape/transactions/domain/TransactionEntity.java new file mode 100644 index 0000000000..b27d5a46ea --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/domain/TransactionEntity.java @@ -0,0 +1,92 @@ +package com.yape.transactions.domain; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.Instant; +import java.util.UUID; + +@Entity +@Table(name = "transactions") +public class TransactionEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "transaction_external_id", nullable = false, unique = true) + private UUID transactionExternalId; + + @Column(name = "account_external_id_debit", nullable = false) + private String accountExternalIdDebit; + + @Column(name = "account_external_id_credit", nullable = false) + private String accountExternalIdCredit; + + @Column(name = "tranfer_type_id", nullable = false) + private Integer tranferTypeId; + + @Column(nullable = false, precision = 18, scale = 2) + private BigDecimal value; + + @Column(nullable = false) + private String status; // pending | approved | rejected + + @Column(name = "created_at", nullable = false) + private Instant createdAt; + + protected TransactionEntity() {} + + public TransactionEntity( + UUID transactionExternalId, + String accountExternalIdDebit, + String accountExternalIdCredit, + Integer tranferTypeId, + BigDecimal value, + String status, + Instant createdAt + ) { + this.transactionExternalId = transactionExternalId; + this.accountExternalIdDebit = accountExternalIdDebit; + this.accountExternalIdCredit = accountExternalIdCredit; + this.tranferTypeId = tranferTypeId; + this.value = value; + this.status = status; + this.createdAt = createdAt; + } + + public Long getId() { + return id; + } + + public UUID getTransactionExternalId() { + return transactionExternalId; + } + + public String getAccountExternalIdDebit() { + return accountExternalIdDebit; + } + + public String getAccountExternalIdCredit() { + return accountExternalIdCredit; + } + + public Integer getTranferTypeId() { + return tranferTypeId; + } + + public BigDecimal getValue() { + return value; + } + + public String getStatus() { + return status; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/transaction/src/main/java/com/yape/transactions/domain/TransactionRepository.java b/transaction/src/main/java/com/yape/transactions/domain/TransactionRepository.java new file mode 100644 index 0000000000..bb400c2383 --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/domain/TransactionRepository.java @@ -0,0 +1,9 @@ +package com.yape.transactions.domain; + +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; +import java.util.UUID; + +public interface TransactionRepository extends JpaRepository { + Optional findByTransactionExternalId(UUID transactionExternalId); +} diff --git a/transaction/src/main/java/com/yape/transactions/kafka/TransactionCreatedEvent.java b/transaction/src/main/java/com/yape/transactions/kafka/TransactionCreatedEvent.java new file mode 100644 index 0000000000..873079d4ea --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/kafka/TransactionCreatedEvent.java @@ -0,0 +1,12 @@ +package com.yape.transactions.kafka; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.math.BigDecimal; + +public record TransactionCreatedEvent( + String transactionExternalId, + String accountExternalIdDebit, + String accountExternalIdCredit, + @JsonProperty("tranferTypeId") Integer tranferTypeId, + BigDecimal value +) {} diff --git a/transaction/src/main/java/com/yape/transactions/kafka/TransactionEventsPublisher.java b/transaction/src/main/java/com/yape/transactions/kafka/TransactionEventsPublisher.java new file mode 100644 index 0000000000..9fc59bafd3 --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/kafka/TransactionEventsPublisher.java @@ -0,0 +1,24 @@ +package com.yape.transactions.kafka; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Component +public class TransactionEventsPublisher { + + private final KafkaTemplate kafkaTemplate; + private final String createdTopic; + + public TransactionEventsPublisher( + KafkaTemplate kafkaTemplate, + @Value("${app.topics.created}") String createdTopic + ) { + this.kafkaTemplate = kafkaTemplate; + this.createdTopic = createdTopic; + } + + public void publishCreated(TransactionCreatedEvent event) { + kafkaTemplate.send(createdTopic, event.transactionExternalId(), event); + } +} diff --git a/transaction/src/main/java/com/yape/transactions/kafka/TransactionStatusEvent.java b/transaction/src/main/java/com/yape/transactions/kafka/TransactionStatusEvent.java new file mode 100644 index 0000000000..cb37f25804 --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/kafka/TransactionStatusEvent.java @@ -0,0 +1,6 @@ +package com.yape.transactions.kafka; + +public record TransactionStatusEvent( + String transactionExternalId, + String status +) {} diff --git a/transaction/src/main/java/com/yape/transactions/kafka/TransactionStatusListener.java b/transaction/src/main/java/com/yape/transactions/kafka/TransactionStatusListener.java new file mode 100644 index 0000000000..a54b6bbfea --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/kafka/TransactionStatusListener.java @@ -0,0 +1,42 @@ +package com.yape.transactions.kafka; + +import com.yape.transactions.domain.TransactionRepository; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import java.util.Locale; +import java.util.UUID; + +@Component +public class TransactionStatusListener { + + private final TransactionRepository repository; + + public TransactionStatusListener(TransactionRepository repository) { + this.repository = repository; + } + + @KafkaListener(topics = "${app.topics.status}", groupId = "transaction-service") + public void onStatus(TransactionStatusEvent event) { + UUID externalId; + try { + externalId = UUID.fromString(event.transactionExternalId()); + } catch (IllegalArgumentException ex) { + return; // ignore malformed event + } + + String normalized = event.status() == null ? null : event.status().toLowerCase(Locale.ROOT); + + if (normalized == null || (!normalized.equals("approved") && !normalized.equals("rejected") && !normalized.equals("pending"))) { + return; // ignore unknown status + } + + repository.findByTransactionExternalId(externalId).ifPresent(tx -> { + // idempotent-ish update + if (!normalized.equals(tx.getStatus())) { + tx.setStatus(normalized); + repository.save(tx); + } + }); + } +} diff --git a/transaction/src/main/java/com/yape/transactions/service/InvalidUuidException.java b/transaction/src/main/java/com/yape/transactions/service/InvalidUuidException.java new file mode 100644 index 0000000000..66b440557f --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/service/InvalidUuidException.java @@ -0,0 +1,7 @@ +package com.yape.transactions.service; + +public class InvalidUuidException extends RuntimeException { + public InvalidUuidException(String raw) { + super("Invalid UUID: " + raw); + } +} diff --git a/transaction/src/main/java/com/yape/transactions/service/TransactionNotFoundException.java b/transaction/src/main/java/com/yape/transactions/service/TransactionNotFoundException.java new file mode 100644 index 0000000000..af9c657dbf --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/service/TransactionNotFoundException.java @@ -0,0 +1,7 @@ +package com.yape.transactions.service; + +public class TransactionNotFoundException extends RuntimeException { + public TransactionNotFoundException(String externalId) { + super("Transaction not found: " + externalId); + } +} diff --git a/transaction/src/main/java/com/yape/transactions/service/TransactionService.java b/transaction/src/main/java/com/yape/transactions/service/TransactionService.java new file mode 100644 index 0000000000..618a6e03a0 --- /dev/null +++ b/transaction/src/main/java/com/yape/transactions/service/TransactionService.java @@ -0,0 +1,76 @@ +package com.yape.transactions.service; + +import com.yape.transactions.api.dto.CreateTransactionRequest; +import com.yape.transactions.api.dto.TransactionResponse; +import com.yape.transactions.domain.TransactionEntity; +import com.yape.transactions.domain.TransactionRepository; +import com.yape.transactions.kafka.TransactionCreatedEvent; +import com.yape.transactions.kafka.TransactionEventsPublisher; +import org.springframework.stereotype.Service; + +import java.time.Instant; +import java.util.UUID; + +@Service +public class TransactionService { + + private final TransactionRepository repository; + private final TransactionEventsPublisher publisher; + + public TransactionService(TransactionRepository repository, TransactionEventsPublisher publisher) { + this.repository = repository; + this.publisher = publisher; + } + + public String createTransaction(CreateTransactionRequest request) { + UUID externalId = UUID.randomUUID(); + + TransactionEntity entity = new TransactionEntity( + externalId, + request.accountExternalIdDebit(), + request.accountExternalIdCredit(), + request.tranferTypeId(), + request.value(), + "pending", + Instant.now() + ); + + repository.save(entity); + + // Emit event for anti-fraud validation + publisher.publishCreated(new TransactionCreatedEvent( + externalId.toString(), + request.accountExternalIdDebit(), + request.accountExternalIdCredit(), + request.tranferTypeId(), + request.value() + )); + + return externalId.toString(); + } + + public TransactionResponse getTransaction(String transactionExternalId) { + UUID id = parseUuid(transactionExternalId); + + TransactionEntity entity = repository.findByTransactionExternalId(id) + .orElseThrow(() -> new TransactionNotFoundException(transactionExternalId)); + + String typeName = "transferType-" + entity.getTranferTypeId(); + + return new TransactionResponse( + entity.getTransactionExternalId().toString(), + new TransactionResponse.NamedDto(typeName), + new TransactionResponse.NamedDto(entity.getStatus()), + entity.getValue(), + entity.getCreatedAt() + ); + } + + private UUID parseUuid(String raw) { + try { + return UUID.fromString(raw); + } catch (IllegalArgumentException ex) { + throw new InvalidUuidException(raw); + } + } +} diff --git a/transaction/src/main/resources/application.yml b/transaction/src/main/resources/application.yml new file mode 100644 index 0000000000..16d214df6b --- /dev/null +++ b/transaction/src/main/resources/application.yml @@ -0,0 +1,34 @@ +server: + port: 8080 + +spring: + datasource: + url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/transactions} + username: ${SPRING_DATASOURCE_USERNAME:postgres} + password: ${SPRING_DATASOURCE_PASSWORD:postgres} + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + jdbc: + time_zone: UTC + + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:29092} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + group-id: transaction-service + auto-offset-reset: earliest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + spring.json.value.default.type: com.yape.transactions.kafka.TransactionStatusEvent + +app: + topics: + created: transactions.created + status: transactions.status diff --git a/transaction/target/classes/application.yml b/transaction/target/classes/application.yml new file mode 100644 index 0000000000..16d214df6b --- /dev/null +++ b/transaction/target/classes/application.yml @@ -0,0 +1,34 @@ +server: + port: 8080 + +spring: + datasource: + url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/transactions} + username: ${SPRING_DATASOURCE_USERNAME:postgres} + password: ${SPRING_DATASOURCE_PASSWORD:postgres} + jpa: + hibernate: + ddl-auto: update + properties: + hibernate: + jdbc: + time_zone: UTC + + kafka: + bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:29092} + producer: + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + group-id: transaction-service + auto-offset-reset: earliest + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.JsonDeserializer + properties: + spring.json.trusted.packages: "*" + spring.json.value.default.type: com.yape.transactions.kafka.TransactionStatusEvent + +app: + topics: + created: transactions.created + status: transactions.status diff --git a/transaction/target/classes/com/yape/transactions/TransactionsApplication.class b/transaction/target/classes/com/yape/transactions/TransactionsApplication.class new file mode 100644 index 0000000000..40c40032f5 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/TransactionsApplication.class differ diff --git a/transaction/target/classes/com/yape/transactions/api/ApiExceptionHandler.class b/transaction/target/classes/com/yape/transactions/api/ApiExceptionHandler.class new file mode 100644 index 0000000000..b31ce7a76e Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/api/ApiExceptionHandler.class differ diff --git a/transaction/target/classes/com/yape/transactions/api/TransactionController.class b/transaction/target/classes/com/yape/transactions/api/TransactionController.class new file mode 100644 index 0000000000..f996d67e6f Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/api/TransactionController.class differ diff --git a/transaction/target/classes/com/yape/transactions/api/dto/CreateTransactionRequest.class b/transaction/target/classes/com/yape/transactions/api/dto/CreateTransactionRequest.class new file mode 100644 index 0000000000..f8ac90eeac Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/api/dto/CreateTransactionRequest.class differ diff --git a/transaction/target/classes/com/yape/transactions/api/dto/CreateTransactionResponse.class b/transaction/target/classes/com/yape/transactions/api/dto/CreateTransactionResponse.class new file mode 100644 index 0000000000..7aa31e2cd9 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/api/dto/CreateTransactionResponse.class differ diff --git a/transaction/target/classes/com/yape/transactions/api/dto/TransactionResponse$NamedDto.class b/transaction/target/classes/com/yape/transactions/api/dto/TransactionResponse$NamedDto.class new file mode 100644 index 0000000000..1ac99cba52 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/api/dto/TransactionResponse$NamedDto.class differ diff --git a/transaction/target/classes/com/yape/transactions/api/dto/TransactionResponse.class b/transaction/target/classes/com/yape/transactions/api/dto/TransactionResponse.class new file mode 100644 index 0000000000..5ec4fadfb3 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/api/dto/TransactionResponse.class differ diff --git a/transaction/target/classes/com/yape/transactions/domain/TransactionEntity.class b/transaction/target/classes/com/yape/transactions/domain/TransactionEntity.class new file mode 100644 index 0000000000..05222be2d1 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/domain/TransactionEntity.class differ diff --git a/transaction/target/classes/com/yape/transactions/domain/TransactionRepository.class b/transaction/target/classes/com/yape/transactions/domain/TransactionRepository.class new file mode 100644 index 0000000000..20f23bc2e0 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/domain/TransactionRepository.class differ diff --git a/transaction/target/classes/com/yape/transactions/kafka/TransactionCreatedEvent.class b/transaction/target/classes/com/yape/transactions/kafka/TransactionCreatedEvent.class new file mode 100644 index 0000000000..d23c156773 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/kafka/TransactionCreatedEvent.class differ diff --git a/transaction/target/classes/com/yape/transactions/kafka/TransactionEventsPublisher.class b/transaction/target/classes/com/yape/transactions/kafka/TransactionEventsPublisher.class new file mode 100644 index 0000000000..a8bc7f2266 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/kafka/TransactionEventsPublisher.class differ diff --git a/transaction/target/classes/com/yape/transactions/kafka/TransactionStatusEvent.class b/transaction/target/classes/com/yape/transactions/kafka/TransactionStatusEvent.class new file mode 100644 index 0000000000..26e000237b Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/kafka/TransactionStatusEvent.class differ diff --git a/transaction/target/classes/com/yape/transactions/kafka/TransactionStatusListener.class b/transaction/target/classes/com/yape/transactions/kafka/TransactionStatusListener.class new file mode 100644 index 0000000000..fc9e1c61f8 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/kafka/TransactionStatusListener.class differ diff --git a/transaction/target/classes/com/yape/transactions/service/InvalidUuidException.class b/transaction/target/classes/com/yape/transactions/service/InvalidUuidException.class new file mode 100644 index 0000000000..7ef2f14f19 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/service/InvalidUuidException.class differ diff --git a/transaction/target/classes/com/yape/transactions/service/TransactionNotFoundException.class b/transaction/target/classes/com/yape/transactions/service/TransactionNotFoundException.class new file mode 100644 index 0000000000..6d8a810ad1 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/service/TransactionNotFoundException.class differ diff --git a/transaction/target/classes/com/yape/transactions/service/TransactionService.class b/transaction/target/classes/com/yape/transactions/service/TransactionService.class new file mode 100644 index 0000000000..b4b28ef8b1 Binary files /dev/null and b/transaction/target/classes/com/yape/transactions/service/TransactionService.class differ