diff --git a/.gitignore b/.gitignore index 67045665db..bed58806b7 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,15 @@ dist # TernJS port file .tern-port + + +*.class +*.jar + +#Maven +target/ +dist/ + +# JetBrains IDE +.idea/ +.iml/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000000..ab1f4164ed --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000000..40c03a4e0e --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000000..63e9001932 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000000..712ab9d985 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000000..9dc782bb2d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000000..966e525331 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000000..35eb1ddfbb --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..8dea6c227c --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip diff --git a/app/app.iml b/app/app.iml new file mode 100644 index 0000000000..a80f8afc3c --- /dev/null +++ b/app/app.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/pom.xml b/app/pom.xml new file mode 100644 index 0000000000..67a3009db5 --- /dev/null +++ b/app/pom.xml @@ -0,0 +1,67 @@ + + 4.0.0 + + + com.yape.challenge + transaction-system + 1.0.0 + + + app + + + + com.yape.challenge + core + 1.0.0 + + + 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 + + + org.springframework.kafka + spring-kafka + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.5.0 + + + org.mapstruct + mapstruct + ${mapstruct.version} + + + org.projectlombok + lombok + ${lombok.version} + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/app/src/main/java/com/challenge/transaction/app/TransactionApplication.java b/app/src/main/java/com/challenge/transaction/app/TransactionApplication.java new file mode 100644 index 0000000000..2d6ac21920 --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/TransactionApplication.java @@ -0,0 +1,12 @@ +package com.challenge.transaction.app; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class TransactionApplication { + + public static void main(String[] args) { + SpringApplication.run(TransactionApplication.class, args); + } +} diff --git a/app/src/main/java/com/challenge/transaction/app/config/BeanConfiguration.java b/app/src/main/java/com/challenge/transaction/app/config/BeanConfiguration.java new file mode 100644 index 0000000000..1f4fffac94 --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/config/BeanConfiguration.java @@ -0,0 +1,34 @@ +package com.challenge.transaction.app.config; + +import com.challenge.transaction.core.ports.input.CreateTransactionUseCase; +import com.challenge.transaction.core.ports.input.GetTransactionUseCase; +import com.challenge.transaction.core.ports.output.TransactionEventPublisher; +import com.challenge.transaction.core.ports.output.TransactionRepository; +import com.challenge.transaction.core.usecase.CreateTransactionService; +import com.challenge.transaction.core.usecase.GetTransactionService; +import com.challenge.transaction.core.usecase.UpdateTransactionStatusService; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class BeanConfiguration { + + @Bean + public CreateTransactionUseCase createTransactionUseCase( + TransactionRepository repository, + TransactionEventPublisher publisher) { + return new CreateTransactionService(repository, publisher); + } + + @Bean + public GetTransactionUseCase getTransactionUseCase( + TransactionRepository repository) { + return new GetTransactionService(repository); + } + + @Bean + public UpdateTransactionStatusService updateTransactionStatusService( + TransactionRepository repository) { + return new UpdateTransactionStatusService(repository); + } +} diff --git a/app/src/main/java/com/challenge/transaction/app/config/KafkaConfig.java b/app/src/main/java/com/challenge/transaction/app/config/KafkaConfig.java new file mode 100644 index 0000000000..e8e5416cd5 --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/config/KafkaConfig.java @@ -0,0 +1,19 @@ +package com.challenge.transaction.app.config; + +import org.apache.kafka.clients.admin.NewTopic; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class KafkaConfig { + + @Bean + public NewTopic transactionCreatedTopic() { + return new NewTopic("transaction-created", 3, (short) 1); + } + + @Bean + public NewTopic transactionValidatedTopic() { + return new NewTopic("transaction-validated", 3, (short) 1); + } +} diff --git a/app/src/main/java/com/challenge/transaction/app/config/SwaggerConfig.java b/app/src/main/java/com/challenge/transaction/app/config/SwaggerConfig.java new file mode 100644 index 0000000000..ed7f13584f --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/config/SwaggerConfig.java @@ -0,0 +1,22 @@ +package com.challenge.transaction.app.config; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI transactionApi() { + + return new OpenAPI() + .info(new Info() + .title("Transaction API") + .description("Reto de Yape") + .version("1.0")); + + } + +} diff --git a/app/src/main/java/com/challenge/transaction/app/controller/TransactionController.java b/app/src/main/java/com/challenge/transaction/app/controller/TransactionController.java new file mode 100644 index 0000000000..7dc457c9b0 --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/controller/TransactionController.java @@ -0,0 +1,38 @@ +package com.challenge.transaction.app.controller; + +import com.challenge.transaction.app.dto.CreateTransactionRequest; +import com.challenge.transaction.app.dto.TransactionResponse; +import com.challenge.transaction.app.mapper.TransactionMapper; +import com.challenge.transaction.core.domain.model.Transaction; +import com.challenge.transaction.core.ports.input.CreateTransactionUseCase; +import com.challenge.transaction.core.ports.input.GetTransactionUseCase; +import lombok.AllArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/transactions") +@AllArgsConstructor +public class TransactionController { + + private final CreateTransactionUseCase createUseCase; + private final GetTransactionUseCase getUseCase; + + @PostMapping + public ResponseEntity create( + @RequestBody CreateTransactionRequest request) { + Transaction transaction = TransactionMapper.MAPPER.toDomain(request); + Transaction saved = createUseCase.create(transaction); + + return ResponseEntity.ok(TransactionMapper.MAPPER.toResponse(saved)); + } + + @GetMapping("/{id}") + public ResponseEntity get(@PathVariable UUID id) { + Transaction transaction = getUseCase.get(id); + + return ResponseEntity.ok(TransactionMapper.MAPPER.toResponse(transaction)); + } +} diff --git a/app/src/main/java/com/challenge/transaction/app/dto/CreateTransactionRequest.java b/app/src/main/java/com/challenge/transaction/app/dto/CreateTransactionRequest.java new file mode 100644 index 0000000000..33f38feb08 --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/dto/CreateTransactionRequest.java @@ -0,0 +1,21 @@ +package com.challenge.transaction.app.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serial; +import java.io.Serializable; +import java.util.UUID; + +@Getter +@Setter +public class CreateTransactionRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 3741074636662158145L; + + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private Integer transferTypeId; + private Double value; +} diff --git a/app/src/main/java/com/challenge/transaction/app/dto/TransactionResponse.java b/app/src/main/java/com/challenge/transaction/app/dto/TransactionResponse.java new file mode 100644 index 0000000000..52ffd1a52a --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/dto/TransactionResponse.java @@ -0,0 +1,41 @@ +package com.challenge.transaction.app.dto; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +public class TransactionResponse implements Serializable { + + @Serial + private static final long serialVersionUID = -5230394309771679808L; + + private UUID transactionExternalId; + private TypeDto transactionType; + private StatusDto transactionStatus; + private Double value; + private LocalDateTime createdAt; + + @Getter + @Setter + public static class StatusDto implements Serializable { + @Serial + private static final long serialVersionUID = 3144852181997944515L; + + private String name; + } + + @Getter + @Setter + public static class TypeDto implements Serializable { + @Serial + private static final long serialVersionUID = -5686189229013063853L; + + private String name; + } +} diff --git a/app/src/main/java/com/challenge/transaction/app/kafka/FraudResultConsumer.java b/app/src/main/java/com/challenge/transaction/app/kafka/FraudResultConsumer.java new file mode 100644 index 0000000000..ed95ac3967 --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/kafka/FraudResultConsumer.java @@ -0,0 +1,47 @@ +package com.challenge.transaction.app.kafka; + +import com.challenge.transaction.core.domain.enums.TransactionStatus; +import com.challenge.transaction.core.domain.event.FraudResultEvent; +import com.challenge.transaction.core.domain.model.Transaction; +import com.challenge.transaction.core.usecase.UpdateTransactionStatusService; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@Slf4j +@Component +@AllArgsConstructor +public class FraudResultConsumer { + private final UpdateTransactionStatusService service; + private final FraudResultProducer producer; + + @KafkaListener(topics = "transaction-created") + public void consume(Transaction transaction) { + String status; + + if (transaction.getValue() > 1000) { + status = TransactionStatus.REJECTED.name(); + } else { + status = TransactionStatus.APPROVED.name(); + } + + producer.publish(transaction.getTransactionExternalId().toString(), status); + } + + @KafkaListener(topics = "transaction-validated") + public void consume(FraudResultEvent event) { + log.info("[TRANSACTION-VALIDATED] init..."); + log.info("[TRANSACTION-VALIDATED] transactionId: {}",event.getTransactionId()); + + service.updateStatus( + UUID.fromString(event.getTransactionId()), + event.getStatus() + ); + + log.info("[TRANSACTION-VALIDATED] finished."); + } + +} diff --git a/app/src/main/java/com/challenge/transaction/app/kafka/FraudResultProducer.java b/app/src/main/java/com/challenge/transaction/app/kafka/FraudResultProducer.java new file mode 100644 index 0000000000..04c46dc5d8 --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/kafka/FraudResultProducer.java @@ -0,0 +1,28 @@ +package com.challenge.transaction.app.kafka; + +import com.challenge.transaction.core.domain.event.FraudResultEvent; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class FraudResultProducer { + + private final KafkaTemplate kafkaTemplate; + + public void publish(String transactionId, String status) { + log.info("[FRAUD-VALIDATION] init..."); + log.info("[FRAUD-VALIDATION] transactionId = {}", transactionId); + + FraudResultEvent event = new FraudResultEvent(); + event.setTransactionId(transactionId); + event.setStatus(status); + + kafkaTemplate.send("transaction-validated", event); + + log.info("[FRAUD-VALIDATION] valid finish."); + } +} diff --git a/app/src/main/java/com/challenge/transaction/app/kafka/TransactionProducer.java b/app/src/main/java/com/challenge/transaction/app/kafka/TransactionProducer.java new file mode 100644 index 0000000000..ed17ca726f --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/kafka/TransactionProducer.java @@ -0,0 +1,26 @@ +package com.challenge.transaction.app.kafka; + +import com.challenge.transaction.core.domain.model.Transaction; +import com.challenge.transaction.core.ports.output.TransactionEventPublisher; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@AllArgsConstructor +public class TransactionProducer implements TransactionEventPublisher { + + private final KafkaTemplate kafkaTemplate; + + @Override + public void publishTransactionCreated(Transaction transaction) { + log.info("[TRANSACTION-SEND] init..."); + log.info("[TRANSACTION-SEND] transactionId: {}", transaction.getTransactionExternalId()); + + kafkaTemplate.send("transaction-created", transaction); + + log.info("[TRANSACTION-SEND] init..."); + } +} diff --git a/app/src/main/java/com/challenge/transaction/app/mapper/TransactionMapper.java b/app/src/main/java/com/challenge/transaction/app/mapper/TransactionMapper.java new file mode 100644 index 0000000000..1a4802b0cd --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/mapper/TransactionMapper.java @@ -0,0 +1,44 @@ +package com.challenge.transaction.app.mapper; + +import com.challenge.transaction.app.dto.CreateTransactionRequest; +import com.challenge.transaction.app.dto.TransactionResponse; +import com.challenge.transaction.core.domain.enums.TransactionStatus; +import com.challenge.transaction.core.domain.model.Transaction; +import com.challenge.transaction.core.domain.model.TransactionType; +import org.mapstruct.Mapping; +import org.mapstruct.Named; +import org.mapstruct.factory.Mappers; +import org.springframework.stereotype.Component; + +@Component +public interface TransactionMapper { + TransactionMapper MAPPER = Mappers.getMapper(TransactionMapper.class); + + @Mapping(target = "transactionType", source = "transferTypeId", qualifiedByName = "toTransactionType") + Transaction toDomain(CreateTransactionRequest request); + + @Mapping(target = "transactionStatus", source = "status", qualifiedByName = "toTransactionStatus") + @Mapping(target = "transactionType", source = "transactionType", qualifiedByName = "toTransactionTypeName") + TransactionResponse toResponse(Transaction transaction); + + @Named("toTransactionStatus") + default TransactionResponse.StatusDto toTransactionStatus(TransactionStatus status) { + TransactionResponse.StatusDto statusDto = new TransactionResponse.StatusDto(); + statusDto.setName(status.name()); + + return statusDto; + } + + @Named("toTransactionType") + default TransactionType toTransactionType(Integer transferTypeId) { + return TransactionType.fromId(transferTypeId); + } + + @Named("toTransactionTypeName") + default TransactionResponse.TypeDto toTransactionTypeName(TransactionType transactionType) { + TransactionResponse.TypeDto typeDto = new TransactionResponse.TypeDto(); + typeDto.setName(transactionType.name()); + + return typeDto; + } +} diff --git a/app/src/main/java/com/challenge/transaction/app/persistence/TransactionEntity.java b/app/src/main/java/com/challenge/transaction/app/persistence/TransactionEntity.java new file mode 100644 index 0000000000..d0064a4d3c --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/persistence/TransactionEntity.java @@ -0,0 +1,31 @@ +package com.challenge.transaction.app.persistence; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.Setter; +import org.springframework.data.annotation.Id; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.UUID; + +@Entity +@Table(name = "transactions") +@Getter +@Setter +public class TransactionEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 4397230746145932537L; + + @Id + private UUID transactionExternalId; + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private Integer transferTypeId; + private Double value; + private String status; + private LocalDateTime createdAt; +} diff --git a/app/src/main/java/com/challenge/transaction/app/persistence/TransactionJpaRepository.java b/app/src/main/java/com/challenge/transaction/app/persistence/TransactionJpaRepository.java new file mode 100644 index 0000000000..47a1fec50d --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/persistence/TransactionJpaRepository.java @@ -0,0 +1,9 @@ +package com.challenge.transaction.app.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface TransactionJpaRepository + extends JpaRepository { +} diff --git a/app/src/main/java/com/challenge/transaction/app/persistence/TransactionRepositoryAdapter.java b/app/src/main/java/com/challenge/transaction/app/persistence/TransactionRepositoryAdapter.java new file mode 100644 index 0000000000..366303ddbb --- /dev/null +++ b/app/src/main/java/com/challenge/transaction/app/persistence/TransactionRepositoryAdapter.java @@ -0,0 +1,56 @@ +package com.challenge.transaction.app.persistence; + +import com.challenge.transaction.core.domain.enums.TransactionStatus; +import com.challenge.transaction.core.domain.model.Transaction; +import com.challenge.transaction.core.ports.output.TransactionRepository; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Optional; +import java.util.UUID; + +@Slf4j +@Component +@AllArgsConstructor +public class TransactionRepositoryAdapter implements TransactionRepository { + + private final TransactionJpaRepository repository; + + @Override + public Transaction save(Transaction transaction) { + log.info("[TRANSACTION-SAVE] init..."); + TransactionEntity entity = new TransactionEntity(); + + entity.setTransactionExternalId(transaction.getTransactionExternalId()); + entity.setValue(transaction.getValue()); + entity.setStatus(transaction.getStatus().name()); + entity.setCreatedAt(transaction.getCreatedAt()); + + repository.save(entity); + + log.info("[TRANSACTION-SAVE] transactionExternalId: {}", transaction.getTransactionExternalId()); + log.info("[TRANSACTION-SAVE] end."); + + return transaction; + } + + @Override + public Optional findByExternalId(UUID id) { + return repository.findById(id).map(e -> { + Transaction t = new Transaction(); + t.setTransactionExternalId(e.getTransactionExternalId()); + t.setValue(e.getValue()); + return t; + }); + } + + @Override + public void updateStatus(UUID id, String status) { + + repository.findById(id).ifPresent(entity -> { + entity.setStatus(status); + repository.save(entity); + }); + } +} diff --git a/app/src/main/resources/application.yaml b/app/src/main/resources/application.yaml new file mode 100644 index 0000000000..3c70d423a7 --- /dev/null +++ b/app/src/main/resources/application.yaml @@ -0,0 +1,12 @@ +spring: + datasource: + url: jdbc:postgresql://localhost:5432/transactions + username: postgres + password: postgres + + jpa: + hibernate: + ddl-auto: update + + kafka: + bootstrap-servers: localhost:9092 diff --git a/core/core.iml b/core/core.iml new file mode 100644 index 0000000000..a80f8afc3c --- /dev/null +++ b/core/core.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 0000000000..5d6be521d6 --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + + com.yape.challenge + transaction-system + 1.0.0 + + + core + + + + org.projectlombok + lombok + 1.18.32 + provided + + + jakarta.validation + jakarta.validation-api + + + diff --git a/core/src/main/java/com/challenge/transaction/core/domain/enums/TransactionStatus.java b/core/src/main/java/com/challenge/transaction/core/domain/enums/TransactionStatus.java new file mode 100644 index 0000000000..75a7c14072 --- /dev/null +++ b/core/src/main/java/com/challenge/transaction/core/domain/enums/TransactionStatus.java @@ -0,0 +1,7 @@ +package com.challenge.transaction.core.domain.enums; + +public enum TransactionStatus { + PENDING, + APPROVED, + REJECTED +} diff --git a/core/src/main/java/com/challenge/transaction/core/domain/event/FraudResultEvent.java b/core/src/main/java/com/challenge/transaction/core/domain/event/FraudResultEvent.java new file mode 100644 index 0000000000..f90f885dc7 --- /dev/null +++ b/core/src/main/java/com/challenge/transaction/core/domain/event/FraudResultEvent.java @@ -0,0 +1,18 @@ +package com.challenge.transaction.core.domain.event; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serial; +import java.io.Serializable; + +@Getter +@Setter +public class FraudResultEvent implements Serializable { + + @Serial + private static final long serialVersionUID = 7775625147330100345L; + + private String transactionId; + private String status; +} diff --git a/core/src/main/java/com/challenge/transaction/core/domain/model/Transaction.java b/core/src/main/java/com/challenge/transaction/core/domain/model/Transaction.java new file mode 100644 index 0000000000..98752fb35d --- /dev/null +++ b/core/src/main/java/com/challenge/transaction/core/domain/model/Transaction.java @@ -0,0 +1,26 @@ +package com.challenge.transaction.core.domain.model; + +import com.challenge.transaction.core.domain.enums.TransactionStatus; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.UUID; + +@Getter +@Setter +public class Transaction implements Serializable { + + @Serial + private static final long serialVersionUID = -1915736314429727217L; + + private UUID transactionExternalId; + private UUID accountExternalIdDebit; + private UUID accountExternalIdCredit; + private TransactionType transactionType; + private Double value; + private TransactionStatus status; + private LocalDateTime createdAt; +} diff --git a/core/src/main/java/com/challenge/transaction/core/domain/model/TransactionType.java b/core/src/main/java/com/challenge/transaction/core/domain/model/TransactionType.java new file mode 100644 index 0000000000..994115fe28 --- /dev/null +++ b/core/src/main/java/com/challenge/transaction/core/domain/model/TransactionType.java @@ -0,0 +1,27 @@ +package com.challenge.transaction.core.domain.model; + +import lombok.Getter; + +@Getter +public enum TransactionType { + + TRANSFER(1); + + private final int id; + + TransactionType(int id) { + this.id = id; + } + + public static TransactionType fromId(int id) { + + for (TransactionType type : values()) { + if (type.id == id) { + return type; + } + } + + throw new IllegalArgumentException("Tipo de transaccion no registrada"); + } + +} diff --git a/core/src/main/java/com/challenge/transaction/core/ports/input/CreateTransactionUseCase.java b/core/src/main/java/com/challenge/transaction/core/ports/input/CreateTransactionUseCase.java new file mode 100644 index 0000000000..68e9edaf32 --- /dev/null +++ b/core/src/main/java/com/challenge/transaction/core/ports/input/CreateTransactionUseCase.java @@ -0,0 +1,7 @@ +package com.challenge.transaction.core.ports.input; + +import com.challenge.transaction.core.domain.model.Transaction; + +public interface CreateTransactionUseCase { + Transaction create(Transaction transaction); +} diff --git a/core/src/main/java/com/challenge/transaction/core/ports/input/GetTransactionUseCase.java b/core/src/main/java/com/challenge/transaction/core/ports/input/GetTransactionUseCase.java new file mode 100644 index 0000000000..8d24e9db17 --- /dev/null +++ b/core/src/main/java/com/challenge/transaction/core/ports/input/GetTransactionUseCase.java @@ -0,0 +1,9 @@ +package com.challenge.transaction.core.ports.input; + +import com.challenge.transaction.core.domain.model.Transaction; + +import java.util.UUID; + +public interface GetTransactionUseCase { + Transaction get(UUID transactionExternalId); +} diff --git a/core/src/main/java/com/challenge/transaction/core/ports/output/TransactionEventPublisher.java b/core/src/main/java/com/challenge/transaction/core/ports/output/TransactionEventPublisher.java new file mode 100644 index 0000000000..492f68e3d5 --- /dev/null +++ b/core/src/main/java/com/challenge/transaction/core/ports/output/TransactionEventPublisher.java @@ -0,0 +1,7 @@ +package com.challenge.transaction.core.ports.output; + +import com.challenge.transaction.core.domain.model.Transaction; + +public interface TransactionEventPublisher { + void publishTransactionCreated(Transaction transaction); +} diff --git a/core/src/main/java/com/challenge/transaction/core/ports/output/TransactionRepository.java b/core/src/main/java/com/challenge/transaction/core/ports/output/TransactionRepository.java new file mode 100644 index 0000000000..4aae30f1c8 --- /dev/null +++ b/core/src/main/java/com/challenge/transaction/core/ports/output/TransactionRepository.java @@ -0,0 +1,14 @@ +package com.challenge.transaction.core.ports.output; + +import com.challenge.transaction.core.domain.model.Transaction; + +import java.util.Optional; +import java.util.UUID; + +public interface TransactionRepository { + Transaction save(Transaction transaction); + + Optional findByExternalId(UUID transactionExternalId); + + void updateStatus(UUID transactionExternalId, String status); +} diff --git a/core/src/main/java/com/challenge/transaction/core/usecase/CreateTransactionService.java b/core/src/main/java/com/challenge/transaction/core/usecase/CreateTransactionService.java new file mode 100644 index 0000000000..2ddfbd3cab --- /dev/null +++ b/core/src/main/java/com/challenge/transaction/core/usecase/CreateTransactionService.java @@ -0,0 +1,32 @@ +package com.challenge.transaction.core.usecase; + +import com.challenge.transaction.core.domain.enums.TransactionStatus; +import com.challenge.transaction.core.domain.model.Transaction; +import com.challenge.transaction.core.ports.input.CreateTransactionUseCase; +import com.challenge.transaction.core.ports.output.TransactionEventPublisher; +import com.challenge.transaction.core.ports.output.TransactionRepository; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; +import java.util.UUID; + +@RequiredArgsConstructor +public class CreateTransactionService implements CreateTransactionUseCase { + + private final TransactionRepository repository; + private final TransactionEventPublisher publisher; + + @Override + public Transaction create(Transaction transaction) { + transaction.setTransactionExternalId(UUID.randomUUID()); + transaction.setStatus(TransactionStatus.PENDING); + transaction.setCreatedAt(LocalDateTime.now()); + + Transaction saved = repository.save(transaction); + + publisher.publishTransactionCreated(saved); + + return saved; + } + +} diff --git a/core/src/main/java/com/challenge/transaction/core/usecase/GetTransactionService.java b/core/src/main/java/com/challenge/transaction/core/usecase/GetTransactionService.java new file mode 100644 index 0000000000..83d39cd737 --- /dev/null +++ b/core/src/main/java/com/challenge/transaction/core/usecase/GetTransactionService.java @@ -0,0 +1,20 @@ +package com.challenge.transaction.core.usecase; + +import com.challenge.transaction.core.domain.model.Transaction; +import com.challenge.transaction.core.ports.input.GetTransactionUseCase; +import com.challenge.transaction.core.ports.output.TransactionRepository; +import lombok.AllArgsConstructor; + +import java.util.UUID; + +@AllArgsConstructor +public class GetTransactionService implements GetTransactionUseCase { + private final TransactionRepository repository; + + @Override + public Transaction get(UUID transactionExternalId) { + return repository.findByExternalId(transactionExternalId) + .orElseThrow(() -> new RuntimeException("Transaction not found")); + } + +} diff --git a/core/src/main/java/com/challenge/transaction/core/usecase/UpdateTransactionStatusService.java b/core/src/main/java/com/challenge/transaction/core/usecase/UpdateTransactionStatusService.java new file mode 100644 index 0000000000..4258fcf3ff --- /dev/null +++ b/core/src/main/java/com/challenge/transaction/core/usecase/UpdateTransactionStatusService.java @@ -0,0 +1,16 @@ +package com.challenge.transaction.core.usecase; + +import com.challenge.transaction.core.ports.output.TransactionRepository; +import lombok.AllArgsConstructor; + +import java.util.UUID; + +@AllArgsConstructor +public class UpdateTransactionStatusService { + + private final TransactionRepository repository; + + public void updateStatus(UUID transactionId, String status) { + repository.updateStatus(transactionId, status); + } +} diff --git a/core/src/test/java/com/challenge/transaction/core/usercase/CreateTransactionServiceTest.java b/core/src/test/java/com/challenge/transaction/core/usercase/CreateTransactionServiceTest.java new file mode 100644 index 0000000000..14f88695ea --- /dev/null +++ b/core/src/test/java/com/challenge/transaction/core/usercase/CreateTransactionServiceTest.java @@ -0,0 +1,45 @@ +package com.challenge.transaction.core.usercase; + +import com.challenge.transaction.core.domain.model.Transaction; +import com.challenge.transaction.core.ports.output.TransactionEventPublisher; +import com.challenge.transaction.core.ports.output.TransactionRepository; +import com.challenge.transaction.core.usecase.CreateTransactionService; +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 static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class CreateTransactionServiceTest { + + @Mock + private TransactionRepository repository; + + @Mock + private TransactionEventPublisher publisher; + + @InjectMocks + private CreateTransactionService service; + + @Test + void shouldCreateTransactionAndPublishEvent() { + + Transaction transaction = new Transaction(); + transaction.setValue(100.0); + + when(repository.save(any())).thenReturn(transaction); + + Transaction result = service.create(transaction); + + verify(repository).save(any()); + verify(publisher).publishTransactionCreated(any()); + + assertNotNull(result); + } +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000000..3b32b09987 --- /dev/null +++ b/pom.xml @@ -0,0 +1,65 @@ + + 4.0.0 + + com.yape.challenge + transaction-system + 1.0.0 + pom + + + core + app + + + + 21 + 3.3.2 + 1.5.5.Final + 1.18.32 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.mockito + mockito-junit-jupiter + test + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + ${java.version} + ${java.version} + + + + + +