diff --git a/README.md b/README.md new file mode 100644 index 00000000..acf5d402 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +# MBA Hexagonal Architecture — Plataforma de Ingressos + +Projeto do curso de Arquitetura Hexagonal & Clean Architecture (Full Cycle), estendido com a feature de **cancelamento de evento**. + +## Como subir o projeto + +Pré-requisitos: Java 17 e Docker (para o MySQL). + +1. Suba o banco de dados: + ```bash + docker-compose up -d + ``` +2. Rode a aplicação: + ```bash + ./gradlew :infrastructure:bootRun + ``` + A API sobe em `http://localhost:8080`. O endpoint REST fica na raiz (`/events`, `/customers`, `/partners`) e o GraphQL em `/graphql` (GraphiQL habilitado em `/graphiql`). + +## Como rodar a suíte de testes + +```bash +./gradlew test +``` + +Isso roda os testes de domínio (`domain`), casos de uso com repositórios in-memory (`application`) e os testes de integração/REST (`infrastructure`), estes últimos usando um banco H2 em memória (perfil `test`), sem depender do MySQL do `docker-compose`. + +Para rodar só um módulo: `./gradlew :domain:test`, `./gradlew :application:test` ou `./gradlew :infrastructure:test`. + +## Coleção de requisições `.http` (opcional, uso local) + +Crie localmente uma pasta `requests/` com arquivos `.http` (extensão [REST Client](https://marketplace.visualstudio.com/items?itemName=humao.rest-client) do VS Code) para testar a API manualmente — um arquivo por entidade (`partners.http`, `customers.http`, `events.http`), com exemplos de criação/cancelamento/consulta de evento. Essa pasta está no `.gitignore` por ser uma conveniência de desenvolvimento, não um entregável do desafio. + +## Onde acontece a cascata de cancelamento + +Quando um parceiro cancela um evento (`CancelEventUseCase`), o agregado `Event` apenas transiciona seu próprio estado para `CANCELLED` e registra o evento de domínio `EventCancelled` (`domain/.../event/EventCancelled.java`) — ele **não** toca o agregado `Ticket` nem chama nenhum caso de uso de ingresso de forma síncrona. + +Esse `EventCancelled` trafega pelo mesmo mecanismo já usado para `EventTicketReserved`: ao persistir o evento (`EventDatabaseRepository`), os eventos de domínio pendentes são gravados na tabela `outbox`; o job `OutboxRelay` publica periodicamente os registros não publicados na fila via `QueueGateway`; e o `ConsumerQueueGateway` roteia a mensagem pelo seu `type` (`event.cancelled`) para o caso de uso `CancelEventTicketsUseCase`, que busca todos os ingressos do evento (`TicketRepository.ticketsByEventId`) e cancela cada um (`Ticket.cancel()`, idempotente). + +Esse fluxo assíncrono ponta a ponta — publicação no `ConsumerQueueGateway` até os tickets ficarem `CANCELLED` — é coberto pelo teste `infrastructure/src/test/java/br/com/fullcycle/infrastructure/gateways/ConsumerQueueGatewayCancelEventIT.java`. diff --git a/application/build.gradle.kts b/application/build.gradle.kts new file mode 100644 index 00000000..c6e07c4e --- /dev/null +++ b/application/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + `java-conventions` + `java-library` +} + +group = "br.com.fullcycle.application" + +dependencies { + implementation(project(":domain")) +} \ No newline at end of file diff --git a/application/src/main/java/br/com/fullcycle/application/NullaryUseCase.java b/application/src/main/java/br/com/fullcycle/application/NullaryUseCase.java new file mode 100644 index 00000000..719299b3 --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/NullaryUseCase.java @@ -0,0 +1,17 @@ +package br.com.fullcycle.application; + +public abstract class NullaryUseCase { + + // 1. Cada caso de uso tem um Input e um Output próprio. Não retorna a entidade, o agregado, ou objeto de valor. + // 2. O caso de uso implementa o padrão Command + + public abstract OUTPUT execute(); + + public T execute(Presenter presenter) { + try { + return presenter.present(execute()); + } catch (Throwable t) { + return presenter.present(t); + } + } +} diff --git a/application/src/main/java/br/com/fullcycle/application/Presenter.java b/application/src/main/java/br/com/fullcycle/application/Presenter.java new file mode 100644 index 00000000..f4066959 --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/Presenter.java @@ -0,0 +1,8 @@ +package br.com.fullcycle.application; + +public interface Presenter { + + OUT present(IN input); + + OUT present(Throwable error); +} diff --git a/application/src/main/java/br/com/fullcycle/application/UnitUseCase.java b/application/src/main/java/br/com/fullcycle/application/UnitUseCase.java new file mode 100644 index 00000000..cd14f22c --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/UnitUseCase.java @@ -0,0 +1,9 @@ +package br.com.fullcycle.application; + +public abstract class UnitUseCase { + + // 1. Cada caso de uso tem um Input e um Output próprio. Não retorna a entidade, o agregado, ou objeto de valor. + // 2. O caso de uso implementa o padrão Command + + public abstract void execute(INPUT input); +} diff --git a/application/src/main/java/br/com/fullcycle/application/UseCase.java b/application/src/main/java/br/com/fullcycle/application/UseCase.java new file mode 100644 index 00000000..bbb6eeda --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/UseCase.java @@ -0,0 +1,17 @@ +package br.com.fullcycle.application; + +public abstract class UseCase { + + // 1. Cada caso de uso tem um Input e um Output próprio. Não retorna a entidade, o agregado, ou objeto de valor. + // 2. O caso de uso implementa o padrão Command + + public abstract OUTPUT execute(INPUT input); + + public T execute(INPUT input, Presenter presenter) { + try { + return presenter.present(execute(input)); + } catch (Throwable t) { + return presenter.present(t); + } + } +} diff --git a/application/src/main/java/br/com/fullcycle/application/customer/CreateCustomerUseCase.java b/application/src/main/java/br/com/fullcycle/application/customer/CreateCustomerUseCase.java new file mode 100644 index 00000000..5afc486b --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/customer/CreateCustomerUseCase.java @@ -0,0 +1,44 @@ +package br.com.fullcycle.application.customer; + +import br.com.fullcycle.application.UseCase; +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.customer.CustomerRepository; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.person.Cpf; +import br.com.fullcycle.domain.person.Email; + +public class CreateCustomerUseCase + extends UseCase { + + private final CustomerRepository customerRepository; + + public CreateCustomerUseCase(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + @Override + public Output execute(final Input input) { + if (customerRepository.customerOfCPF(new Cpf(input.cpf)).isPresent()) { + throw new ValidationException("Customer already exists"); + } + + if (customerRepository.customerOfEmail(new Email(input.email)).isPresent()) { + throw new ValidationException("Customer already exists"); + } + + var customer = customerRepository.create(Customer.newCustomer(input.name, input.cpf, input.email)); + + return new Output( + customer.customerId().value(), + customer.cpf().value(), + customer.email().value(), + customer.name().value() + ); + } + + public record Input(String cpf, String email, String name) { + } + + public record Output(String id, String cpf, String email, String name) { + } +} diff --git a/application/src/main/java/br/com/fullcycle/application/customer/GetCustomerByIdUseCase.java b/application/src/main/java/br/com/fullcycle/application/customer/GetCustomerByIdUseCase.java new file mode 100644 index 00000000..88788dc1 --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/customer/GetCustomerByIdUseCase.java @@ -0,0 +1,35 @@ +package br.com.fullcycle.application.customer; + +import br.com.fullcycle.application.UseCase; +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.customer.CustomerRepository; + +import java.util.Objects; +import java.util.Optional; + +public class GetCustomerByIdUseCase + extends UseCase> { + + private final CustomerRepository customerRepository; + + public GetCustomerByIdUseCase(final CustomerRepository customerRepository) { + this.customerRepository = Objects.requireNonNull(customerRepository); + } + + @Override + public Optional execute(final Input input) { + return customerRepository.customerOfId(CustomerId.with(input.id)) + .map(c -> new Output( + c.customerId().value(), + c.cpf().value(), + c.email().value(), + c.name().value()) + ); + } + + public record Input(String id) { + } + + public record Output(String id, String cpf, String email, String name) { + } +} diff --git a/application/src/main/java/br/com/fullcycle/application/event/CancelEventUseCase.java b/application/src/main/java/br/com/fullcycle/application/event/CancelEventUseCase.java new file mode 100644 index 00000000..0f62d431 --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/event/CancelEventUseCase.java @@ -0,0 +1,35 @@ +package br.com.fullcycle.application.event; + +import br.com.fullcycle.application.UseCase; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.EventRepository; +import br.com.fullcycle.domain.exceptions.ValidationException; + +import java.util.Objects; + +public class CancelEventUseCase extends UseCase { + + private final EventRepository eventRepository; + + public CancelEventUseCase(final EventRepository eventRepository) { + this.eventRepository = Objects.requireNonNull(eventRepository); + } + + @Override + public Output execute(final Input input) { + final var anEvent = eventRepository.eventOfId(EventId.with(input.id())) + .orElseThrow(() -> new ValidationException("Event not found")); + + anEvent.cancel(); + + eventRepository.update(anEvent); + + return new Output(anEvent.eventId().value(), anEvent.status().name()); + } + + public record Input(String id) { + } + + public record Output(String id, String status) { + } +} diff --git a/application/src/main/java/br/com/fullcycle/application/event/CreateEventUseCase.java b/application/src/main/java/br/com/fullcycle/application/event/CreateEventUseCase.java new file mode 100644 index 00000000..f753a394 --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/event/CreateEventUseCase.java @@ -0,0 +1,45 @@ +package br.com.fullcycle.application.event; + +import br.com.fullcycle.application.UseCase; +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.event.EventRepository; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.partner.PartnerId; +import br.com.fullcycle.domain.partner.PartnerRepository; + +import java.util.Objects; + +public class CreateEventUseCase extends UseCase { + + private final EventRepository eventRepository; + private final PartnerRepository partnerRepository; + + public CreateEventUseCase(final EventRepository eventRepository, final PartnerRepository partnerRepository) { + this.eventRepository = Objects.requireNonNull(eventRepository); + this.partnerRepository = Objects.requireNonNull(partnerRepository); + } + + @Override + public Output execute(final Input input) { + final var aPartner = partnerRepository.partnerOfId(PartnerId.with(input.partnerId)) + .orElseThrow(() -> new ValidationException("Partner not found")); + + final var anEvent = + eventRepository.create(Event.newEvent(input.name, input.date, input.totalSpots, aPartner)); + + return new Output( + anEvent.eventId().value(), + input.date, + anEvent.name().value(), + anEvent.totalSpots(), + anEvent.partnerId().value(), + anEvent.status().name() + ); + } + + public record Input(String date, String name, String partnerId, Integer totalSpots) { + } + + public record Output(String id, String date, String name, int totalSpots, String partnerId, String status) { + } +} diff --git a/application/src/main/java/br/com/fullcycle/application/event/GetEventByIdUseCase.java b/application/src/main/java/br/com/fullcycle/application/event/GetEventByIdUseCase.java new file mode 100644 index 00000000..984a1aa1 --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/event/GetEventByIdUseCase.java @@ -0,0 +1,37 @@ +package br.com.fullcycle.application.event; + +import br.com.fullcycle.application.UseCase; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.EventRepository; + +import java.time.format.DateTimeFormatter; +import java.util.Objects; +import java.util.Optional; + +public class GetEventByIdUseCase + extends UseCase> { + + private final EventRepository eventRepository; + + public GetEventByIdUseCase(final EventRepository eventRepository) { + this.eventRepository = Objects.requireNonNull(eventRepository); + } + + @Override + public Optional execute(final Input input) { + return eventRepository.eventOfId(EventId.with(input.id())) + .map(anEvent -> new Output( + anEvent.eventId().value(), + anEvent.name().value(), + anEvent.date().format(DateTimeFormatter.ISO_LOCAL_DATE), + anEvent.totalSpots(), + anEvent.status().name()) + ); + } + + public record Input(String id) { + } + + public record Output(String id, String name, String date, int totalSpots, String status) { + } +} diff --git a/application/src/main/java/br/com/fullcycle/application/event/SubscribeCustomerToEventUseCase.java b/application/src/main/java/br/com/fullcycle/application/event/SubscribeCustomerToEventUseCase.java new file mode 100644 index 00000000..9abbcbcd --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/event/SubscribeCustomerToEventUseCase.java @@ -0,0 +1,48 @@ +package br.com.fullcycle.application.event; + +import br.com.fullcycle.application.UseCase; +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.customer.CustomerRepository; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.EventRepository; +import br.com.fullcycle.domain.event.EventTicket; +import br.com.fullcycle.domain.event.ticket.TicketRepository; +import br.com.fullcycle.domain.exceptions.ValidationException; + +import java.time.Instant; +import java.util.Objects; + +public class SubscribeCustomerToEventUseCase extends UseCase { + + private final CustomerRepository customerRepository; + private final EventRepository eventRepository; + + public SubscribeCustomerToEventUseCase( + final CustomerRepository customerRepository, + final EventRepository eventRepository + ) { + this.customerRepository = Objects.requireNonNull(customerRepository); + this.eventRepository = Objects.requireNonNull(eventRepository); + } + + @Override + public Output execute(final Input input) { + var aCustomer = customerRepository.customerOfId(CustomerId.with(input.customerId())) + .orElseThrow(() -> new ValidationException("Customer not found")); + + var anEvent = eventRepository.eventOfId(EventId.with(input.eventId())) + .orElseThrow(() -> new ValidationException("Event not found")); + + final EventTicket ticket = anEvent.reserveTicket(aCustomer.customerId()); + + eventRepository.update(anEvent); + + return new Output(anEvent.eventId().value(), ticket.eventTicketId().value(), Instant.now()); + } + + public record Input(String customerId, String eventId) { + } + + public record Output(String eventId, String eventTicketId, Instant reservationDate) { + } +} diff --git a/application/src/main/java/br/com/fullcycle/application/partner/CreatePartnerUseCase.java b/application/src/main/java/br/com/fullcycle/application/partner/CreatePartnerUseCase.java new file mode 100644 index 00000000..74fd70a7 --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/partner/CreatePartnerUseCase.java @@ -0,0 +1,45 @@ +package br.com.fullcycle.application.partner; + +import br.com.fullcycle.application.UseCase; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.partner.PartnerRepository; +import br.com.fullcycle.domain.person.Cnpj; +import br.com.fullcycle.domain.person.Email; + +import java.util.Objects; + +public class CreatePartnerUseCase extends UseCase { + + private final PartnerRepository partnerRepository; + + public CreatePartnerUseCase(final PartnerRepository partnerRepository) { + this.partnerRepository = Objects.requireNonNull(partnerRepository); + } + + @Override + public Output execute(final Input input) { + if (partnerRepository.partnerOfCNPJ(new Cnpj(input.cnpj)).isPresent()) { + throw new ValidationException("Partner already exists"); + } + + if (partnerRepository.partnerOfEmail(new Email(input.email)).isPresent()) { + throw new ValidationException("Partner already exists"); + } + + var partner = partnerRepository.create(Partner.newPartner(input.name, input.cnpj, input.email)); + + return new Output( + partner.partnerId().value(), + partner.cnpj().value(), + partner.email().value(), + partner.name().value() + ); + } + + public record Input(String cnpj, String email, String name) { + } + + public record Output(String id, String cnpj, String email, String name) { + } +} diff --git a/application/src/main/java/br/com/fullcycle/application/partner/GetPartnerByIdUseCase.java b/application/src/main/java/br/com/fullcycle/application/partner/GetPartnerByIdUseCase.java new file mode 100644 index 00000000..0dd286c7 --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/partner/GetPartnerByIdUseCase.java @@ -0,0 +1,35 @@ +package br.com.fullcycle.application.partner; + +import br.com.fullcycle.application.UseCase; +import br.com.fullcycle.domain.partner.PartnerId; +import br.com.fullcycle.domain.partner.PartnerRepository; + +import java.util.Objects; +import java.util.Optional; + +public class GetPartnerByIdUseCase + extends UseCase> { + + private final PartnerRepository partnerRepository; + + public GetPartnerByIdUseCase(final PartnerRepository partnerRepository) { + this.partnerRepository = Objects.requireNonNull(partnerRepository); + } + + @Override + public Optional execute(final Input input) { + return partnerRepository.partnerOfId(PartnerId.with(input.id)) + .map(partner -> new Output( + partner.partnerId().value(), + partner.cnpj().value(), + partner.email().value(), + partner.name().value() + )); + } + + public record Input(String id) { + } + + public record Output(String id, String cnpj, String email, String name) { + } +} diff --git a/application/src/main/java/br/com/fullcycle/application/ticket/CancelEventTicketsUseCase.java b/application/src/main/java/br/com/fullcycle/application/ticket/CancelEventTicketsUseCase.java new file mode 100644 index 00000000..5e22eff3 --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/ticket/CancelEventTicketsUseCase.java @@ -0,0 +1,39 @@ +package br.com.fullcycle.application.ticket; + +import br.com.fullcycle.application.UseCase; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.ticket.TicketRepository; + +import java.util.List; +import java.util.Objects; + +public class CancelEventTicketsUseCase + extends UseCase { + + private final TicketRepository ticketRepository; + + public CancelEventTicketsUseCase(final TicketRepository ticketRepository) { + this.ticketRepository = Objects.requireNonNull(ticketRepository); + } + + @Override + public Output execute(final Input input) { + final var anEventId = EventId.with(input.eventId()); + + final var cancelledTicketIds = ticketRepository.ticketsByEventId(anEventId).stream() + .map(aTicket -> { + aTicket.cancel(); + ticketRepository.update(aTicket); + return aTicket.ticketId().value(); + }) + .toList(); + + return new Output(input.eventId(), cancelledTicketIds); + } + + public record Input(String eventId) { + } + + public record Output(String eventId, List cancelledTicketIds) { + } +} diff --git a/application/src/main/java/br/com/fullcycle/application/ticket/CreateTicketForCustomerUseCase.java b/application/src/main/java/br/com/fullcycle/application/ticket/CreateTicketForCustomerUseCase.java new file mode 100644 index 00000000..eab6e52f --- /dev/null +++ b/application/src/main/java/br/com/fullcycle/application/ticket/CreateTicketForCustomerUseCase.java @@ -0,0 +1,35 @@ +package br.com.fullcycle.application.ticket; + +import br.com.fullcycle.application.UseCase; +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.EventTicketId; +import br.com.fullcycle.domain.event.ticket.Ticket; +import br.com.fullcycle.domain.event.ticket.TicketRepository; + +import java.util.Objects; + +public class CreateTicketForCustomerUseCase + extends UseCase { + + private final TicketRepository ticketRepository; + + public CreateTicketForCustomerUseCase(final TicketRepository ticketRepository) { + this.ticketRepository = Objects.requireNonNull(ticketRepository); + } + + @Override + public Output execute(final Input input) { + + final var aTicket = + Ticket.newTicket(EventTicketId.with(input.eventTicketId), CustomerId.with(input.customerId), EventId.with(input.eventId)); + + this.ticketRepository.create(aTicket); + + return new Output(aTicket.ticketId().value()); + } + + public record Input(String eventTicketId, String eventId, String customerId) {} + + public record Output(String ticketId) {} +} diff --git a/application/src/test/java/br/com/fullcycle/application/customer/CreateCustomerUseCaseTest.java b/application/src/test/java/br/com/fullcycle/application/customer/CreateCustomerUseCaseTest.java new file mode 100644 index 00000000..a4c0ff69 --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/customer/CreateCustomerUseCaseTest.java @@ -0,0 +1,82 @@ +package br.com.fullcycle.application.customer; + +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.application.repository.InMemoryCustomerRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CreateCustomerUseCaseTest { + + @Test + @DisplayName("Deve criar um cliente") + public void testCreateCustomer() { + // given + final var expectedCPF = "123.456.789-01"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + + final var createInput = new CreateCustomerUseCase.Input(expectedCPF, expectedEmail, expectedName); + + final var customerRepository = new InMemoryCustomerRepository(); + + // when + final var useCase = new CreateCustomerUseCase(customerRepository); + final var output = useCase.execute(createInput); + + // then + Assertions.assertNotNull(output.id()); + Assertions.assertEquals(expectedCPF, output.cpf()); + Assertions.assertEquals(expectedEmail, output.email()); + Assertions.assertEquals(expectedName, output.name()); + } + + @Test + @DisplayName("Não deve cadastrar um cliente com CPF duplicado") + public void testCreateWithDuplicatedCPFShouldFail() throws Exception { + // given + final var expectedCPF = "123.456.789-01"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + final var expectedError = "Customer already exists"; + + final var aCustomer = Customer.newCustomer(expectedName, expectedCPF, expectedEmail); + + final var customerRepository = new InMemoryCustomerRepository(); + customerRepository.create(aCustomer); + + final var createInput = new CreateCustomerUseCase.Input(expectedCPF, expectedEmail, expectedName); + + // when + final var useCase = new CreateCustomerUseCase(customerRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(createInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } + + @Test + @DisplayName("Não deve cadastrar um cliente com e-mail duplicado") + public void testCreateWithDuplicatedEmailShouldFail() throws Exception { + // given + final var expectedCPF = "123.456.789-01"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + final var expectedError = "Customer already exists"; + + final var aCustomer = Customer.newCustomer(expectedName, expectedCPF, expectedEmail); + + final var customerRepository = new InMemoryCustomerRepository(); + customerRepository.create(aCustomer); + + final var createInput = new CreateCustomerUseCase.Input(expectedCPF, expectedEmail, expectedName); + + // when + final var useCase = new CreateCustomerUseCase(customerRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(createInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } +} diff --git a/application/src/test/java/br/com/fullcycle/application/customer/GetCustomerByIdUseCaseTest.java b/application/src/test/java/br/com/fullcycle/application/customer/GetCustomerByIdUseCaseTest.java new file mode 100644 index 00000000..e2dbf40d --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/customer/GetCustomerByIdUseCaseTest.java @@ -0,0 +1,57 @@ +package br.com.fullcycle.application.customer; + +import br.com.fullcycle.application.repository.InMemoryCustomerRepository; +import br.com.fullcycle.domain.customer.Customer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +class GetCustomerByIdUseCaseTest { + + @Test + @DisplayName("Deve obter um cliente por id") + public void testGetById() { + // given + final var expectedCPF = "123.456.789-01"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + + final var aCustomer = Customer.newCustomer(expectedName, expectedCPF, expectedEmail); + + final var customerRepository = new InMemoryCustomerRepository(); + customerRepository.create(aCustomer); + + final var expectedID = aCustomer.customerId().value().toString(); + + final var input = new GetCustomerByIdUseCase.Input(expectedID); + + // when + final var useCase = new GetCustomerByIdUseCase(customerRepository); + final var output = useCase.execute(input).get(); + + // then + Assertions.assertEquals(expectedID, output.id()); + Assertions.assertEquals(expectedCPF, output.cpf()); + Assertions.assertEquals(expectedEmail, output.email()); + Assertions.assertEquals(expectedName, output.name()); + } + + @Test + @DisplayName("Deve obter vazio ao tentar recuperar um cliente não existente por id") + public void testGetByIdWIthInvalidId() { + // given + final var expectedID = UUID.randomUUID().toString(); + + final var input = new GetCustomerByIdUseCase.Input(expectedID); + + // when + final var customerRepository = new InMemoryCustomerRepository(); + final var useCase = new GetCustomerByIdUseCase(customerRepository); + final var output = useCase.execute(input); + + // then + Assertions.assertTrue(output.isEmpty()); + } +} \ No newline at end of file diff --git a/application/src/test/java/br/com/fullcycle/application/event/CancelEventUseCaseTest.java b/application/src/test/java/br/com/fullcycle/application/event/CancelEventUseCaseTest.java new file mode 100644 index 00000000..b81e5446 --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/event/CancelEventUseCaseTest.java @@ -0,0 +1,86 @@ +package br.com.fullcycle.application.event; + +import br.com.fullcycle.application.repository.InMemoryEventRepository; +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.partner.Partner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CancelEventUseCaseTest { + + @Test + @DisplayName("Deve cancelar um evento ativo") + public void testCancelEvent() throws Exception { + // given + final var expectedStatus = "CANCELLED"; + + final var aPartner = Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + final var anEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + + final var eventID = anEvent.eventId().value(); + + final var cancelInput = new CancelEventUseCase.Input(eventID); + + final var eventRepository = new InMemoryEventRepository(); + eventRepository.create(anEvent); + + // when + final var useCase = new CancelEventUseCase(eventRepository); + final var output = useCase.execute(cancelInput); + + // then + Assertions.assertEquals(eventID, output.id()); + Assertions.assertEquals(expectedStatus, output.status()); + + final var actualEvent = eventRepository.eventOfId(anEvent.eventId()).get(); + Assertions.assertEquals(expectedStatus, actualEvent.status().name()); + } + + @Test + @DisplayName("Não deve cancelar um evento que não existe") + public void testCancelEventThatDoesNotExist() throws Exception { + // given + final var expectedError = "Event not found"; + + final var eventID = EventId.unique().value(); + + final var cancelInput = new CancelEventUseCase.Input(eventID); + + final var eventRepository = new InMemoryEventRepository(); + + // when + final var useCase = new CancelEventUseCase(eventRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(cancelInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } + + @Test + @DisplayName("Não deve cancelar um evento já cancelado") + public void testCancelEventAlreadyCancelled() throws Exception { + // given + final var expectedError = "Event already cancelled"; + + final var aPartner = Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + final var anEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + anEvent.cancel(); + + final var eventID = anEvent.eventId().value(); + + final var cancelInput = new CancelEventUseCase.Input(eventID); + + final var eventRepository = new InMemoryEventRepository(); + eventRepository.create(anEvent); + + // when + final var useCase = new CancelEventUseCase(eventRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(cancelInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } +} diff --git a/application/src/test/java/br/com/fullcycle/application/event/CreateEventUseCaseTest.java b/application/src/test/java/br/com/fullcycle/application/event/CreateEventUseCaseTest.java new file mode 100644 index 00000000..c5fb76b4 --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/event/CreateEventUseCaseTest.java @@ -0,0 +1,68 @@ +package br.com.fullcycle.application.event; + +import br.com.fullcycle.application.repository.InMemoryEventRepository; +import br.com.fullcycle.application.repository.InMemoryPartnerRepository; +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.partner.PartnerId; +import br.com.fullcycle.domain.exceptions.ValidationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CreateEventUseCaseTest { + + @Test + @DisplayName("Deve criar um evento") + public void testCreate() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + final var expectedDate = "2021-01-01"; + final var expectedName = "Disney on Ice"; + final var expectedTotalSpots = 10; + final var expectedPartnerId = aPartner.partnerId().value(); + + final var createInput = + new CreateEventUseCase.Input(expectedDate, expectedName, expectedPartnerId, expectedTotalSpots); + + final var eventRepository = new InMemoryEventRepository(); + final var partnerRepository = new InMemoryPartnerRepository(); + + partnerRepository.create(aPartner); + + // when + final var useCase = new CreateEventUseCase(eventRepository, partnerRepository); + final var output = useCase.execute(createInput); + + // then + Assertions.assertNotNull(output.id()); + Assertions.assertEquals(expectedDate, output.date()); + Assertions.assertEquals(expectedName, output.name()); + Assertions.assertEquals(expectedTotalSpots, output.totalSpots()); + Assertions.assertEquals(expectedPartnerId, output.partnerId()); + } + + @Test + @DisplayName("Não deve criar um evento quando o Partner não for encontrado") + public void testCreateEvent_whenPartnerDoesntExists_ShouldThrowError() throws Exception { + // given + final var expectedDate = "2021-01-01"; + final var expectedName = "Disney on Ice"; + final var expectedTotalSpots = 10; + final var expectedPartnerId = PartnerId.unique().value(); + final var expectedError = "Partner not found"; + + final var createInput = + new CreateEventUseCase.Input(expectedDate, expectedName, expectedPartnerId, expectedTotalSpots); + + final var eventRepository = new InMemoryEventRepository(); + final var partnerRepository = new InMemoryPartnerRepository(); + + // when + final var useCase = new CreateEventUseCase(eventRepository, partnerRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(createInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } +} \ No newline at end of file diff --git a/application/src/test/java/br/com/fullcycle/application/event/GetEventByIdUseCaseTest.java b/application/src/test/java/br/com/fullcycle/application/event/GetEventByIdUseCaseTest.java new file mode 100644 index 00000000..12537b1b --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/event/GetEventByIdUseCaseTest.java @@ -0,0 +1,52 @@ +package br.com.fullcycle.application.event; + +import br.com.fullcycle.application.repository.InMemoryEventRepository; +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.partner.Partner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class GetEventByIdUseCaseTest { + + @Test + @DisplayName("Deve obter um evento pelo id") + public void testGetEventById() throws Exception { + // given + final var aPartner = Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + final var anEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + + final var eventRepository = new InMemoryEventRepository(); + eventRepository.create(anEvent); + + final var getInput = new GetEventByIdUseCase.Input(anEvent.eventId().value()); + + // when + final var useCase = new GetEventByIdUseCase(eventRepository); + final var output = useCase.execute(getInput); + + // then + Assertions.assertTrue(output.isPresent()); + Assertions.assertEquals(anEvent.eventId().value(), output.get().id()); + Assertions.assertEquals("Disney on Ice", output.get().name()); + Assertions.assertEquals("2021-01-01", output.get().date()); + Assertions.assertEquals(10, output.get().totalSpots()); + Assertions.assertEquals("ACTIVE", output.get().status()); + } + + @Test + @DisplayName("Não deve obter um evento que não existe") + public void testGetEventByIdThatDoesNotExist() throws Exception { + // given + final var eventRepository = new InMemoryEventRepository(); + final var getInput = new GetEventByIdUseCase.Input(EventId.unique().value()); + + // when + final var useCase = new GetEventByIdUseCase(eventRepository); + final var output = useCase.execute(getInput); + + // then + Assertions.assertTrue(output.isEmpty()); + } +} diff --git a/application/src/test/java/br/com/fullcycle/application/event/SubscribeCustomerToEventUseCaseTest.java b/application/src/test/java/br/com/fullcycle/application/event/SubscribeCustomerToEventUseCaseTest.java new file mode 100644 index 00000000..355dce17 --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/event/SubscribeCustomerToEventUseCaseTest.java @@ -0,0 +1,205 @@ +package br.com.fullcycle.application.event; + +import br.com.fullcycle.application.repository.InMemoryCustomerRepository; +import br.com.fullcycle.application.repository.InMemoryEventRepository; +import br.com.fullcycle.application.repository.InMemoryTicketRepository; +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.partner.Partner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class SubscribeCustomerToEventUseCaseTest { + + @Test + @DisplayName("Deve comprar um ticket de um evento") + public void testReserveTicket() throws Exception { + // given + final var expectedTicketsSize = 1; + + final var aPartner = Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + final var anEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + final var aCustomer = Customer.newCustomer("Gabriel Doe", "123.456.789-01", "gabriel.doe@gmail.com"); + + final var customerID = aCustomer.customerId().value(); + final var eventID = anEvent.eventId().value(); + + final var subscribeInput = + new SubscribeCustomerToEventUseCase.Input(customerID, eventID); + + final var customerRepository = new InMemoryCustomerRepository(); + final var eventRepository = new InMemoryEventRepository(); + + customerRepository.create(aCustomer); + eventRepository.create(anEvent); + + // when + final var useCase = new SubscribeCustomerToEventUseCase(customerRepository, eventRepository); + final var output = useCase.execute(subscribeInput); + + // then + Assertions.assertEquals(eventID, output.eventId()); + Assertions.assertNotNull(output.reservationDate()); + + final var actualEvent = eventRepository.eventOfId(anEvent.eventId()); + Assertions.assertEquals(expectedTicketsSize, actualEvent.get().allTickets().size()); + } + + @Test + @DisplayName("Não deve comprar um ticket com um cliente não existente") + public void testReserveTicketWithoutCustomer() throws Exception { + // given + final var expectedError = "Customer not found"; + + final var aPartner = Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + final var anEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + + final var customerID = CustomerId.unique().value(); + final var eventID = anEvent.eventId().value(); + + final var subscribeInput = + new SubscribeCustomerToEventUseCase.Input(customerID, eventID); + + final var customerRepository = new InMemoryCustomerRepository(); + final var eventRepository = new InMemoryEventRepository(); + + eventRepository.create(anEvent); + + // when + final var useCase = new SubscribeCustomerToEventUseCase(customerRepository, eventRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(subscribeInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } + + @Test + @DisplayName("Não deve comprar um ticket de um evento que não existe") + public void testReserveTicketWithoutEvent() throws Exception { + // given + final var expectedError = "Event not found"; + + final var aCustomer = Customer.newCustomer("Gabriel Doe", "123.456.789-01", "gabriel.doe@gmail.com"); + + final var customerID = aCustomer.customerId().value(); + final var eventID = EventId.unique().value(); + + final var subscribeInput = + new SubscribeCustomerToEventUseCase.Input(customerID, eventID); + + final var customerRepository = new InMemoryCustomerRepository(); + final var eventRepository = new InMemoryEventRepository(); + + customerRepository.create(aCustomer); + + // when + final var useCase = new SubscribeCustomerToEventUseCase(customerRepository, eventRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(subscribeInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } + + @Test + @DisplayName("Um mesmo cliente não pode comprar mais de um ticket por evento") + public void testReserveTicketMoreThanOnce() throws Exception { + // given + final var expectedError = "Email already registered"; + + final var aPartner = Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + final var anEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + final var aCustomer = Customer.newCustomer("Gabriel Doe", "123.456.789-01", "gabriel.doe@gmail.com"); + + final var customerID = aCustomer.customerId().value(); + final var eventID = anEvent.eventId().value(); + + final var subscribeInput = + new SubscribeCustomerToEventUseCase.Input(customerID, eventID); + + final var customerRepository = new InMemoryCustomerRepository(); + final var eventRepository = new InMemoryEventRepository(); + + anEvent.reserveTicket(aCustomer.customerId()); + + customerRepository.create(aCustomer); + eventRepository.create(anEvent); + + // when + final var useCase = new SubscribeCustomerToEventUseCase(customerRepository, eventRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(subscribeInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } + + @Test + @DisplayName("Um mesmo cliente não pode comprar de um evento que não há mais cadeiras") + public void testReserveTicketWithoutSlots() throws Exception { + // given + final var expectedError = "Event sold out"; + + final var aPartner = Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + final var anEvent = Event.newEvent("Disney on Ice", "2021-01-01", 1, aPartner); + final var aCustomer = Customer.newCustomer("Gabriel Doe", "123.456.789-01", "gabriel.doe@gmail.com"); + final var aCustomer2 = Customer.newCustomer("Pedro Doe", "123.111.789-01", "pedro.doe@gmail.com"); + + final var customerID = aCustomer.customerId().value(); + final var eventID = anEvent.eventId().value(); + + final var subscribeInput = + new SubscribeCustomerToEventUseCase.Input(customerID, eventID); + + final var customerRepository = new InMemoryCustomerRepository(); + final var eventRepository = new InMemoryEventRepository(); + final var ticketRepository = new InMemoryTicketRepository(); + + anEvent.reserveTicket(aCustomer2.customerId()); + + customerRepository.create(aCustomer); + customerRepository.create(aCustomer2); + eventRepository.create(anEvent); + + // when + final var useCase = new SubscribeCustomerToEventUseCase(customerRepository, eventRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(subscribeInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } + + @Test + @DisplayName("Não deve comprar um ticket de um evento cancelado") + public void testReserveTicketOnCancelledEvent() throws Exception { + // given + final var expectedError = "Event is cancelled"; + + final var aPartner = Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + final var anEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + anEvent.cancel(); + + final var aCustomer = Customer.newCustomer("Gabriel Doe", "123.456.789-01", "gabriel.doe@gmail.com"); + + final var customerID = aCustomer.customerId().value(); + final var eventID = anEvent.eventId().value(); + + final var subscribeInput = + new SubscribeCustomerToEventUseCase.Input(customerID, eventID); + + final var customerRepository = new InMemoryCustomerRepository(); + final var eventRepository = new InMemoryEventRepository(); + + customerRepository.create(aCustomer); + eventRepository.create(anEvent); + + // when + final var useCase = new SubscribeCustomerToEventUseCase(customerRepository, eventRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(subscribeInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } +} \ No newline at end of file diff --git a/application/src/test/java/br/com/fullcycle/application/partner/CreatePartnerUseCaseTest.java b/application/src/test/java/br/com/fullcycle/application/partner/CreatePartnerUseCaseTest.java new file mode 100644 index 00000000..39e15e0d --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/partner/CreatePartnerUseCaseTest.java @@ -0,0 +1,81 @@ +package br.com.fullcycle.application.partner; + +import br.com.fullcycle.application.repository.InMemoryPartnerRepository; +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.exceptions.ValidationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CreatePartnerUseCaseTest { + + @Test + @DisplayName("Deve criar um parceiro") + public void testCreatePartner() { + // given + final var expectedCNPJ = "41.536.538/0001-00"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + + final var createInput = new CreatePartnerUseCase.Input(expectedCNPJ, expectedEmail, expectedName); + + // when + final var partnerRepository = new InMemoryPartnerRepository(); + final var useCase = new CreatePartnerUseCase(partnerRepository); + final var output = useCase.execute(createInput); + + // then + Assertions.assertNotNull(output.id()); + Assertions.assertEquals(expectedCNPJ, output.cnpj()); + Assertions.assertEquals(expectedEmail, output.email()); + Assertions.assertEquals(expectedName, output.name()); + } + + @Test + @DisplayName("Não deve cadastrar um parceiro com CNPJ duplicado") + public void testCreateWithDuplicatedCNPJShouldFail() throws Exception { + // given + final var expectedCNPJ = "41.536.538/0001-00"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + final var expectedError = "Partner already exists"; + + final var aPartner = Partner.newPartner(expectedName, "41.536.538/0002-00", expectedEmail); + + final var partnerRepository = new InMemoryPartnerRepository(); + partnerRepository.create(aPartner); + + final var createInput = new CreatePartnerUseCase.Input(expectedCNPJ, expectedEmail, expectedName); + + // when + final var useCase = new CreatePartnerUseCase(partnerRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(createInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } + + @Test + @DisplayName("Não deve cadastrar um parceiro com e-mail duplicado") + public void testCreateWithDuplicatedEmailShouldFail() throws Exception { + // given + final var expectedCNPJ = "41.536.538/0001-00"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + final var expectedError = "Partner already exists"; + + final var aPartner = Partner.newPartner(expectedName, "41.536.538/0002-00", expectedEmail); + + final var partnerRepository = new InMemoryPartnerRepository(); + partnerRepository.create(aPartner); + + final var createInput = new CreatePartnerUseCase.Input(expectedCNPJ, expectedEmail, expectedName); + + // when + final var useCase = new CreatePartnerUseCase(partnerRepository); + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(createInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } +} diff --git a/application/src/test/java/br/com/fullcycle/application/partner/GetPartnerByIdUseCaseTest.java b/application/src/test/java/br/com/fullcycle/application/partner/GetPartnerByIdUseCaseTest.java new file mode 100644 index 00000000..462f297f --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/partner/GetPartnerByIdUseCaseTest.java @@ -0,0 +1,57 @@ +package br.com.fullcycle.application.partner; + +import br.com.fullcycle.application.repository.InMemoryPartnerRepository; +import br.com.fullcycle.domain.partner.Partner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +class GetPartnerByIdUseCaseTest { + + @Test + @DisplayName("Deve obter um parceiro por id") + public void testGetById() { + // given + final var expectedCNPJ = "41.536.538/0001-00"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + + final var aPartner = Partner.newPartner(expectedName, expectedCNPJ, expectedEmail); + + final var partnerRepository = new InMemoryPartnerRepository(); + partnerRepository.create(aPartner); + + final var expectedID = aPartner.partnerId().value().toString(); + + final var input = new GetPartnerByIdUseCase.Input(expectedID); + + // when + final var useCase = new GetPartnerByIdUseCase(partnerRepository); + final var output = useCase.execute(input).get(); + + // then + Assertions.assertEquals(expectedID, output.id()); + Assertions.assertEquals(expectedCNPJ, output.cnpj()); + Assertions.assertEquals(expectedEmail, output.email()); + Assertions.assertEquals(expectedName, output.name()); + } + + @Test + @DisplayName("Deve obter vazio ao tentar recuperar um parceiro não existente por id") + public void testGetByIdWIthInvalidId() { + // given + final var expectedID = UUID.randomUUID().toString(); + + final var input = new GetPartnerByIdUseCase.Input(expectedID); + + // when + final var partnerRepository = new InMemoryPartnerRepository(); + final var useCase = new GetPartnerByIdUseCase(partnerRepository); + final var output = useCase.execute(input); + + // then + Assertions.assertTrue(output.isEmpty()); + } +} \ No newline at end of file diff --git a/application/src/test/java/br/com/fullcycle/application/repository/InMemoryCustomerRepository.java b/application/src/test/java/br/com/fullcycle/application/repository/InMemoryCustomerRepository.java new file mode 100644 index 00000000..a59490e6 --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/repository/InMemoryCustomerRepository.java @@ -0,0 +1,63 @@ +package br.com.fullcycle.application.repository; + +import br.com.fullcycle.domain.customer.CustomerRepository; +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.person.Cpf; +import br.com.fullcycle.domain.person.Email; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class InMemoryCustomerRepository implements CustomerRepository { + + private final Map customers; + private final Map customersByCPF; + private final Map customersByEmail; + + public InMemoryCustomerRepository() { + this.customers = new HashMap<>(); + this.customersByCPF = new HashMap<>(); + this.customersByEmail = new HashMap<>(); + } + + @Override + public Optional customerOfId(CustomerId anId) { + return Optional.ofNullable(this.customers.get(Objects.requireNonNull(anId).value().toString())); + } + + @Override + public Optional customerOfCPF(Cpf cpf) { + return Optional.ofNullable(this.customersByCPF.get(cpf.value())); + } + + @Override + public Optional customerOfEmail(Email email) { + return Optional.ofNullable(this.customersByEmail.get(email.value())); + } + + @Override + public Customer create(Customer customer) { + this.customers.put(customer.customerId().value().toString(), customer); + this.customersByCPF.put(customer.cpf().value(), customer); + this.customersByEmail.put(customer.email().value(), customer); + return customer; + } + + @Override + public Customer update(Customer customer) { + this.customers.put(customer.customerId().value().toString(), customer); + this.customersByCPF.put(customer.cpf().value(), customer); + this.customersByEmail.put(customer.email().value(), customer); + return customer; + } + + @Override + public void deleteAll() { + this.customers.clear(); + this.customersByCPF.clear(); + this.customersByEmail.clear(); + } +} diff --git a/application/src/test/java/br/com/fullcycle/application/repository/InMemoryEventRepository.java b/application/src/test/java/br/com/fullcycle/application/repository/InMemoryEventRepository.java new file mode 100644 index 00000000..87a370ed --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/repository/InMemoryEventRepository.java @@ -0,0 +1,41 @@ +package br.com.fullcycle.application.repository; + +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.EventRepository; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class InMemoryEventRepository implements EventRepository { + + private final Map events; + + public InMemoryEventRepository() { + this.events = new HashMap<>(); + } + + @Override + public Optional eventOfId(EventId anId) { + return Optional.ofNullable(this.events.get(Objects.requireNonNull(anId).value())); + } + + @Override + public Event create(Event event) { + this.events.put(event.eventId().value(), event); + return event; + } + + @Override + public Event update(Event event) { + this.events.put(event.eventId().value(), event); + return event; + } + + @Override + public void deleteAll() { + this.events.clear(); + } +} diff --git a/application/src/test/java/br/com/fullcycle/application/repository/InMemoryPartnerRepository.java b/application/src/test/java/br/com/fullcycle/application/repository/InMemoryPartnerRepository.java new file mode 100644 index 00000000..dcc49d86 --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/repository/InMemoryPartnerRepository.java @@ -0,0 +1,63 @@ +package br.com.fullcycle.application.repository; + +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.partner.PartnerId; +import br.com.fullcycle.domain.partner.PartnerRepository; +import br.com.fullcycle.domain.person.Cnpj; +import br.com.fullcycle.domain.person.Email; + +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class InMemoryPartnerRepository implements PartnerRepository { + + private final Map partners; + private final Map partnersByCNPJ; + private final Map partnersByEmail; + + public InMemoryPartnerRepository() { + this.partners = new HashMap<>(); + this.partnersByCNPJ = new HashMap<>(); + this.partnersByEmail = new HashMap<>(); + } + + @Override + public Optional partnerOfId(PartnerId anId) { + return Optional.ofNullable(this.partners.get(Objects.requireNonNull(anId).value())); + } + + @Override + public Optional partnerOfCNPJ(Cnpj cnpj) { + return Optional.ofNullable(this.partnersByCNPJ.get(Objects.requireNonNull(cnpj).value())); + } + + @Override + public Optional partnerOfEmail(Email email) { + return Optional.ofNullable(this.partnersByEmail.get(Objects.requireNonNull(email).value())); + } + + @Override + public Partner create(Partner partner) { + this.partners.put(partner.partnerId().value().toString(), partner); + this.partnersByCNPJ.put(partner.cnpj().value(), partner); + this.partnersByEmail.put(partner.email().value(), partner); + return partner; + } + + @Override + public Partner update(Partner partner) { + this.partners.put(partner.partnerId().value().toString(), partner); + this.partnersByCNPJ.put(partner.cnpj().value(), partner); + this.partnersByEmail.put(partner.email().value(), partner); + return partner; + } + + @Override + public void deleteAll() { + this.partners.clear(); + this.partnersByCNPJ.clear(); + this.partnersByEmail.clear(); + } +} diff --git a/application/src/test/java/br/com/fullcycle/application/repository/InMemoryTicketRepository.java b/application/src/test/java/br/com/fullcycle/application/repository/InMemoryTicketRepository.java new file mode 100644 index 00000000..5b955c68 --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/repository/InMemoryTicketRepository.java @@ -0,0 +1,50 @@ +package br.com.fullcycle.application.repository; + +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.ticket.Ticket; +import br.com.fullcycle.domain.event.ticket.TicketId; +import br.com.fullcycle.domain.event.ticket.TicketRepository; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +public class InMemoryTicketRepository implements TicketRepository { + + private final Map tickets; + + public InMemoryTicketRepository() { + this.tickets = new HashMap<>(); + } + + @Override + public Optional ticketOfId(TicketId anId) { + return Optional.ofNullable(this.tickets.get(Objects.requireNonNull(anId).value())); + } + + @Override + public Ticket create(Ticket ticket) { + this.tickets.put(ticket.ticketId().value(), ticket); + return ticket; + } + + @Override + public Ticket update(Ticket ticket) { + this.tickets.put(ticket.ticketId().value(), ticket); + return ticket; + } + + @Override + public void deleteAll() { + this.tickets.clear(); + } + + @Override + public List ticketsByEventId(EventId anEventId) { + return this.tickets.values().stream() + .filter(it -> Objects.equals(it.eventId(), anEventId)) + .toList(); + } +} diff --git a/application/src/test/java/br/com/fullcycle/application/ticket/CancelEventTicketsUseCaseTest.java b/application/src/test/java/br/com/fullcycle/application/ticket/CancelEventTicketsUseCaseTest.java new file mode 100644 index 00000000..a23fbe0a --- /dev/null +++ b/application/src/test/java/br/com/fullcycle/application/ticket/CancelEventTicketsUseCaseTest.java @@ -0,0 +1,86 @@ +package br.com.fullcycle.application.ticket; + +import br.com.fullcycle.application.repository.InMemoryTicketRepository; +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.ticket.Ticket; +import br.com.fullcycle.domain.event.ticket.TicketStatus; +import br.com.fullcycle.domain.partner.Partner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CancelEventTicketsUseCaseTest { + + @Test + @DisplayName("Deve cancelar todos os tickets de um evento") + public void testCancelEventTickets() throws Exception { + // given + final var aPartner = Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + final var anEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + final var aCustomer = Customer.newCustomer("Gabriel Doe", "123.456.789-01", "gabriel.doe@gmail.com"); + final var aCustomer2 = Customer.newCustomer("Pedro Doe", "123.111.789-01", "pedro.doe@gmail.com"); + + final var aTicket = Ticket.newTicket(aCustomer.customerId(), anEvent.eventId()); + final var aTicket2 = Ticket.newTicket(aCustomer2.customerId(), anEvent.eventId()); + + final var ticketRepository = new InMemoryTicketRepository(); + ticketRepository.create(aTicket); + ticketRepository.create(aTicket2); + + final var cancelInput = new CancelEventTicketsUseCase.Input(anEvent.eventId().value()); + + // when + final var useCase = new CancelEventTicketsUseCase(ticketRepository); + final var output = useCase.execute(cancelInput); + + // then + Assertions.assertEquals(2, output.cancelledTicketIds().size()); + + final var tickets = ticketRepository.ticketsByEventId(anEvent.eventId()); + Assertions.assertTrue(tickets.stream().allMatch(it -> it.status() == TicketStatus.CANCELLED)); + } + + @Test + @DisplayName("Deve ser idempotente ao reprocessar o cancelamento dos tickets de um evento") + public void testCancelEventTicketsIsIdempotent() throws Exception { + // given + final var aPartner = Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + final var anEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + final var aCustomer = Customer.newCustomer("Gabriel Doe", "123.456.789-01", "gabriel.doe@gmail.com"); + + final var aTicket = Ticket.newTicket(aCustomer.customerId(), anEvent.eventId()); + + final var ticketRepository = new InMemoryTicketRepository(); + ticketRepository.create(aTicket); + + final var cancelInput = new CancelEventTicketsUseCase.Input(anEvent.eventId().value()); + final var useCase = new CancelEventTicketsUseCase(ticketRepository); + + useCase.execute(cancelInput); + + // when + useCase.execute(cancelInput); + + // then + final var tickets = ticketRepository.ticketsByEventId(anEvent.eventId()); + Assertions.assertEquals(1, tickets.size()); + Assertions.assertEquals(TicketStatus.CANCELLED, tickets.get(0).status()); + } + + @Test + @DisplayName("Não deve falhar ao cancelar os tickets de um evento sem tickets") + public void testCancelEventTicketsWithoutTickets() throws Exception { + // given + final var ticketRepository = new InMemoryTicketRepository(); + final var cancelInput = new CancelEventTicketsUseCase.Input(EventId.unique().value()); + + // when + final var useCase = new CancelEventTicketsUseCase(ticketRepository); + final var output = useCase.execute(cancelInput); + + // then + Assertions.assertTrue(output.cancelledTicketIds().isEmpty()); + } +} diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index acfe3ee1..00000000 --- a/build.gradle.kts +++ /dev/null @@ -1,35 +0,0 @@ -plugins { - java - id("org.springframework.boot") version "3.1.2" - id("io.spring.dependency-management") version "1.1.2" -} - -group = "br.com.fullcycle" -version = "0.0.1-SNAPSHOT" - -java { - sourceCompatibility = JavaVersion.VERSION_17 -} - -repositories { - mavenCentral() -} - -dependencies { - implementation("io.hypersistence:hypersistence-tsid:2.1.0") - implementation("org.springframework.boot:spring-boot-starter-data-jpa") - implementation("org.springframework.boot:spring-boot-starter-graphql") - implementation("org.springframework.boot:spring-boot-starter-web") - - runtimeOnly("com.mysql:mysql-connector-j") - - testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("org.springframework:spring-webflux") - testImplementation("org.springframework.graphql:spring-graphql-test") - - testRuntimeOnly("com.h2database:h2") -} - -tasks.withType { - useJUnitPlatform() -} diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..4e91d6ff --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + `kotlin-dsl` +} + +version = "0.0.1-SNAPSHOT" + +java { + targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_17 +} + +repositories { + mavenCentral() +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/java-conventions.gradle.kts b/buildSrc/src/main/kotlin/java-conventions.gradle.kts new file mode 100644 index 00000000..32b0a5e2 --- /dev/null +++ b/buildSrc/src/main/kotlin/java-conventions.gradle.kts @@ -0,0 +1,26 @@ +plugins { + java + jacoco +} + +java { + targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_17 +} + +repositories { + mavenCentral() +} + +dependencies { + testImplementation(platform("org.junit:junit-bom:5.9.1")) + testImplementation("org.junit.jupiter:junit-jupiter") +} + +jacoco { + toolVersion = "0.8.9" +} + +tasks.test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/domain/build.gradle.kts b/domain/build.gradle.kts new file mode 100644 index 00000000..55e94042 --- /dev/null +++ b/domain/build.gradle.kts @@ -0,0 +1,6 @@ +plugins { + `java-conventions` + `java-library` +} + +group = "br.com.fullcycle.domain" diff --git a/domain/src/main/java/br/com/fullcycle/domain/DomainEvent.java b/domain/src/main/java/br/com/fullcycle/domain/DomainEvent.java new file mode 100644 index 00000000..7a664b73 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/DomainEvent.java @@ -0,0 +1,12 @@ +package br.com.fullcycle.domain; + +import java.time.Instant; + +public interface DomainEvent { + + String domainEventId(); + + String type(); + + Instant occurredOn(); +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/customer/Customer.java b/domain/src/main/java/br/com/fullcycle/domain/customer/Customer.java new file mode 100644 index 00000000..e7807acc --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/customer/Customer.java @@ -0,0 +1,73 @@ +package br.com.fullcycle.domain.customer; + +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.person.Cpf; +import br.com.fullcycle.domain.person.Email; +import br.com.fullcycle.domain.person.Name; + +import java.util.Objects; + +public class Customer { + + private final CustomerId customerId; + private Name name; + private Cpf cpf; + private Email email; + + public Customer(final CustomerId customerId, final String name, final String cpf, final String email) { + if (customerId == null) { + throw new ValidationException("Invalid customerId for Customer"); + } + + this.customerId = customerId; + this.setName(name); + this.setCpf(cpf); + this.setEmail(email); + } + + public static Customer newCustomer(String name, String cpf, String email) { + return new Customer(CustomerId.unique(), name, cpf, email); + } + + public CustomerId customerId() { + return customerId; + } + + public Name name() { + return name; + } + + public Cpf cpf() { + return cpf; + } + + public Email email() { + return email; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Customer customer = (Customer) o; + return Objects.equals(customerId, customer.customerId); + } + + @Override + public int hashCode() { + return Objects.hash(customerId); + } + + private void setCpf(final String cpf) { + this.cpf = new Cpf(cpf); + } + + private void setEmail(final String email) { + this.email = new Email(email); + } + + private void setName(final String name) { + this.name = new Name(name); + } + +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/customer/CustomerId.java b/domain/src/main/java/br/com/fullcycle/domain/customer/CustomerId.java new file mode 100644 index 00000000..71e8e17b --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/customer/CustomerId.java @@ -0,0 +1,26 @@ +package br.com.fullcycle.domain.customer; + +import br.com.fullcycle.domain.exceptions.ValidationException; + +import java.util.UUID; + +public record CustomerId(String value) { + + public CustomerId { + if (value == null) { + throw new ValidationException("Invalid value for CustomerId"); + } + } + + public static CustomerId unique() { + return new CustomerId(UUID.randomUUID().toString()); + } + + public static CustomerId with(final String value) { + try { + return new CustomerId(UUID.fromString(value).toString()); + } catch (IllegalArgumentException ex) { + throw new ValidationException("Invalid value for CustomerId"); + } + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/customer/CustomerRepository.java b/domain/src/main/java/br/com/fullcycle/domain/customer/CustomerRepository.java new file mode 100644 index 00000000..41a5265a --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/customer/CustomerRepository.java @@ -0,0 +1,21 @@ +package br.com.fullcycle.domain.customer; + +import br.com.fullcycle.domain.person.Cpf; +import br.com.fullcycle.domain.person.Email; + +import java.util.Optional; + +public interface CustomerRepository { + + Optional customerOfId(CustomerId anId); + + Optional customerOfCPF(Cpf cpf); + + Optional customerOfEmail(Email email); + + Customer create(Customer customer); + + Customer update(Customer customer); + + void deleteAll(); +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/Event.java b/domain/src/main/java/br/com/fullcycle/domain/event/Event.java new file mode 100644 index 00000000..3ac1d53a --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/Event.java @@ -0,0 +1,184 @@ +package br.com.fullcycle.domain.event; + +import br.com.fullcycle.domain.DomainEvent; +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.partner.PartnerId; +import br.com.fullcycle.domain.person.Name; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public class Event { + + private static final int ONE = 1; + + private final EventId eventId; + private final Set tickets; + private final Set domainEvents; + + private Name name; + private LocalDate date; + private int totalSpots; + private PartnerId partnerId; + private EventStatus status; + + public Event( + final EventId eventId, + final String name, + final String date, + final Integer totalSpots, + final PartnerId partnerId, + final EventStatus status, + final Set tickets + ) { + this(eventId, tickets); + this.setName(name); + this.setDate(date); + this.setTotalSpots(totalSpots); + this.setPartnerId(partnerId); + this.status = status != null ? status : EventStatus.ACTIVE; + } + + private Event(final EventId eventId, final Set tickets) { + if (eventId == null) { + throw new ValidationException("Invalid eventId for Event"); + } + + this.eventId = eventId; + this.tickets = tickets != null ? tickets : new HashSet<>(0); + this.domainEvents = new HashSet<>(2); + } + + public static Event newEvent(final String name, final String date, final Integer totalSpots, final Partner partner) { + return new Event(EventId.unique(), name, date, totalSpots, partner.partnerId(), EventStatus.ACTIVE, null); + } + + public static Event restore( + final String id, + final String name, + final String date, + final int totalSpots, + final String partnerId, + final String status, + final Set tickets + ) { + return new Event(EventId.with(id), name, date, totalSpots, PartnerId.with(partnerId), EventStatus.valueOf(status), tickets); + } + + public void cancel() { + if (this.status == EventStatus.CANCELLED) { + throw new ValidationException("Event already cancelled"); + } + + this.status = EventStatus.CANCELLED; + this.domainEvents.add(new EventCancelled(eventId())); + } + + public EventTicket reserveTicket(final CustomerId aCustomerId) { + if (this.status == EventStatus.CANCELLED) { + throw new ValidationException("Event is cancelled"); + } + + this.allTickets().stream() + .filter(it -> Objects.equals(it.customerId(), aCustomerId)) + .findFirst() + .ifPresent(it -> { + throw new ValidationException("Email already registered"); + }); + + if (totalSpots() < allTickets().size() + ONE) { + throw new ValidationException("Event sold out"); + } + + final var aTicket = + EventTicket.newTicket(eventId(), aCustomerId, allTickets().size() + 1); + + this.tickets.add(aTicket); + this.domainEvents.add(new EventTicketReserved(aTicket.eventTicketId(), eventId(), aCustomerId)); + + return aTicket; + } + + public EventId eventId() { + return eventId; + } + + public Name name() { + return name; + } + + public LocalDate date() { + return date; + } + + public int totalSpots() { + return totalSpots; + } + + public PartnerId partnerId() { + return partnerId; + } + + public EventStatus status() { + return status; + } + + public Set allTickets() { + return Collections.unmodifiableSet(tickets); + } + + public Set allDomainEvents() { + return Collections.unmodifiableSet(domainEvents); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Event event = (Event) o; + return Objects.equals(eventId, event.eventId); + } + + @Override + public int hashCode() { + return Objects.hash(eventId); + } + + private void setName(final String name) { + this.name = new Name(name); + } + + private void setDate(final String date) { + if (date == null) { + throw new ValidationException("Invalid date for Event"); + } + + try { + this.date = LocalDate.parse(date, DateTimeFormatter.ISO_LOCAL_DATE); + } catch (RuntimeException ex) { + throw new ValidationException("Invalid date for Event", ex); + } + } + + private void setPartnerId(final PartnerId partnerId) { + if (partnerId == null) { + throw new ValidationException("Invalid totalSpots for Event"); + } + + this.partnerId = partnerId; + } + + private void setTotalSpots(final Integer totalSpots) { + if (totalSpots == null) { + throw new ValidationException("Invalid totalSpots for Event"); + } + + this.totalSpots = totalSpots; + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/EventCancelled.java b/domain/src/main/java/br/com/fullcycle/domain/event/EventCancelled.java new file mode 100644 index 00000000..7591ee5b --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/EventCancelled.java @@ -0,0 +1,18 @@ +package br.com.fullcycle.domain.event; + +import br.com.fullcycle.domain.DomainEvent; + +import java.time.Instant; +import java.util.UUID; + +public record EventCancelled( + String domainEventId, + String type, + String eventId, + Instant occurredOn +) implements DomainEvent { + + public EventCancelled(EventId eventId) { + this(UUID.randomUUID().toString(), "event.cancelled", eventId.value(), Instant.now()); + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/EventId.java b/domain/src/main/java/br/com/fullcycle/domain/event/EventId.java new file mode 100644 index 00000000..a7905518 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/EventId.java @@ -0,0 +1,26 @@ +package br.com.fullcycle.domain.event; + +import br.com.fullcycle.domain.exceptions.ValidationException; + +import java.util.UUID; + +public record EventId(String value) { + + public EventId { + if (value == null) { + throw new ValidationException("Invalid value for EventId"); + } + } + + public static EventId unique() { + return new EventId(UUID.randomUUID().toString()); + } + + public static EventId with(final String value) { + try { + return new EventId(UUID.fromString(value).toString()); + } catch (IllegalArgumentException ex) { + throw new ValidationException("Invalid value for EventId"); + } + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/EventRepository.java b/domain/src/main/java/br/com/fullcycle/domain/event/EventRepository.java new file mode 100644 index 00000000..24c4c20c --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/EventRepository.java @@ -0,0 +1,14 @@ +package br.com.fullcycle.domain.event; + +import java.util.Optional; + +public interface EventRepository { + + Optional eventOfId(EventId anId); + + Event create(Event event); + + Event update(Event event); + + void deleteAll(); +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/EventStatus.java b/domain/src/main/java/br/com/fullcycle/domain/event/EventStatus.java new file mode 100644 index 00000000..0b41c528 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/EventStatus.java @@ -0,0 +1,5 @@ +package br.com.fullcycle.domain.event; + +public enum EventStatus { + ACTIVE, CANCELLED; +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/EventTicket.java b/domain/src/main/java/br/com/fullcycle/domain/event/EventTicket.java new file mode 100644 index 00000000..24847c90 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/EventTicket.java @@ -0,0 +1,71 @@ +package br.com.fullcycle.domain.event; + +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.event.ticket.TicketId; +import br.com.fullcycle.domain.exceptions.ValidationException; + +public class EventTicket { + + private final EventTicketId eventTicketId; + private final EventId eventId; + private final CustomerId customerId; + private TicketId ticketId; + private int ordering; + + public EventTicket(final EventTicketId eventTicketId, final EventId eventId, final CustomerId customerId, final TicketId ticketId, final Integer ordering) { + if (eventTicketId == null) { + throw new ValidationException("Invalid eventTicketId for EventTicket"); + } + + if (eventId == null) { + throw new ValidationException("Invalid eventId for EventTicket"); + } + + if (customerId == null) { + throw new ValidationException("Invalid customerId for EventTicket"); + } + + this.eventTicketId = eventTicketId; + this.eventId = eventId; + this.customerId = customerId; + this.ticketId = ticketId; + this.setOrdering(ordering); + } + + public static EventTicket newTicket(final EventId eventId, final CustomerId customerId, final int ordering) { + return new EventTicket(EventTicketId.unique(), eventId, customerId, null, ordering); + } + + public EventTicket associateTicket(final TicketId aTicket) { + this.ticketId = aTicket; + return this; + } + + public EventTicketId eventTicketId() { + return eventTicketId; + } + + public TicketId ticketId() { + return ticketId; + } + + public EventId eventId() { + return eventId; + } + + public int ordering() { + return ordering; + } + + public CustomerId customerId() { + return customerId; + } + + private void setOrdering(final Integer ordering) { + if (ordering == null) { + throw new ValidationException("Invalid ordering for EventTicket"); + } + + this.ordering = ordering; + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/EventTicketId.java b/domain/src/main/java/br/com/fullcycle/domain/event/EventTicketId.java new file mode 100644 index 00000000..8c769413 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/EventTicketId.java @@ -0,0 +1,26 @@ +package br.com.fullcycle.domain.event; + +import br.com.fullcycle.domain.exceptions.ValidationException; + +import java.util.UUID; + +public record EventTicketId(String value) { + + public EventTicketId { + if (value == null) { + throw new ValidationException("Invalid value for EventTicketId"); + } + } + + public static EventTicketId unique() { + return new EventTicketId(UUID.randomUUID().toString()); + } + + public static EventTicketId with(final String value) { + try { + return new EventTicketId(UUID.fromString(value).toString()); + } catch (IllegalArgumentException ex) { + throw new ValidationException("Invalid value for EventTicketId"); + } + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/EventTicketReserved.java b/domain/src/main/java/br/com/fullcycle/domain/event/EventTicketReserved.java new file mode 100644 index 00000000..77d66aa4 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/EventTicketReserved.java @@ -0,0 +1,21 @@ +package br.com.fullcycle.domain.event; + +import br.com.fullcycle.domain.DomainEvent; +import br.com.fullcycle.domain.customer.CustomerId; + +import java.time.Instant; +import java.util.UUID; + +public record EventTicketReserved( + String domainEventId, + String type, + String eventTicketId, + String eventId, + String customerId, + Instant occurredOn +) implements DomainEvent { + + public EventTicketReserved(EventTicketId eventTicketId, EventId eventId, CustomerId customerId) { + this(UUID.randomUUID().toString(), "event-ticket.reserved", eventTicketId.value(), eventId.value(), customerId.value(), Instant.now()); + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/ticket/Ticket.java b/domain/src/main/java/br/com/fullcycle/domain/event/ticket/Ticket.java new file mode 100644 index 00000000..a56984d6 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/ticket/Ticket.java @@ -0,0 +1,137 @@ +package br.com.fullcycle.domain.event.ticket; + +import br.com.fullcycle.domain.DomainEvent; +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.EventTicketId; +import br.com.fullcycle.domain.exceptions.ValidationException; + +import java.time.Instant; +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +public class Ticket { + + private final TicketId ticketId; + private final Set domainEvents; + + private CustomerId customerId; + private EventId eventId; + private TicketStatus status; + private Instant paidAt; + private Instant reservedAt; + + public Ticket( + final TicketId ticketId, + final CustomerId customerId, + final EventId eventId, + final TicketStatus status, + final Instant paidAt, + final Instant reservedAt + ) { + this.ticketId = ticketId; + this.domainEvents = new HashSet<>(); + this.setCustomerId(customerId); + this.setEventId(eventId); + this.setStatus(status); + this.setPaidAt(paidAt); + this.setReservedAt(reservedAt); + } + + public static Ticket newTicket(final CustomerId customerId, final EventId eventId) { + return new Ticket(TicketId.unique(), customerId, eventId, TicketStatus.PENDING, null, Instant.now()); + } + + public static Ticket newTicket(final EventTicketId eventTicketId, final CustomerId customerId, final EventId eventId) { + final var aTicket = newTicket(customerId, eventId); + aTicket.domainEvents.add(new TicketCreated(aTicket.ticketId, eventTicketId, eventId, customerId)); + return aTicket; + } + + public TicketId ticketId() { + return ticketId; + } + + public CustomerId customerId() { + return customerId; + } + + public EventId eventId() { + return eventId; + } + + public TicketStatus status() { + return status; + } + + public Instant paidAt() { + return paidAt; + } + + public Instant reservedAt() { + return reservedAt; + } + + public Set allDomainEvents() { + return Collections.unmodifiableSet(domainEvents); + } + + public void cancel() { + if (this.status == TicketStatus.CANCELLED) { + return; + } + + this.status = TicketStatus.CANCELLED; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Ticket ticket = (Ticket) o; + return Objects.equals(ticketId, ticket.ticketId); + } + + @Override + public int hashCode() { + return Objects.hash(ticketId); + } + + private void setCustomerId(final CustomerId customerId) { + if (customerId == null) { + throw new ValidationException("Invalid customerId for Ticket"); + } + + this.customerId = customerId; + } + + private void setEventId(final EventId eventId) { + if (eventId == null) { + throw new ValidationException("Invalid eventId for Ticket"); + } + + this.eventId = eventId; + } + + private void setStatus(final TicketStatus status) { + if (status == null) { + throw new ValidationException("Invalid status for Ticket"); + } + + this.status = status; + } + + private void setPaidAt(Instant paidAt) { + this.paidAt = paidAt; + } + + private void setReservedAt(Instant reservedAt) { + if (reservedAt == null) { + throw new ValidationException("Invalid reservedAt for Ticket"); + } + + this.reservedAt = reservedAt; + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketCreated.java b/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketCreated.java new file mode 100644 index 00000000..f60c07b1 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketCreated.java @@ -0,0 +1,24 @@ +package br.com.fullcycle.domain.event.ticket; + +import br.com.fullcycle.domain.DomainEvent; +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.EventTicketId; + +import java.time.Instant; +import java.util.UUID; + +public record TicketCreated( + String domainEventId, + String type, + String ticketId, + String eventTicketId, + String eventId, + String customerId, + Instant occurredOn +) implements DomainEvent { + + public TicketCreated(TicketId ticketId, EventTicketId eventTicketId, EventId eventId, CustomerId customerId) { + this(UUID.randomUUID().toString(), "ticket.created", ticketId.value(), eventTicketId.value(), eventId.value(), customerId.value(), Instant.now()); + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketId.java b/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketId.java new file mode 100644 index 00000000..aa702225 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketId.java @@ -0,0 +1,26 @@ +package br.com.fullcycle.domain.event.ticket; + +import br.com.fullcycle.domain.exceptions.ValidationException; + +import java.util.UUID; + +public record TicketId(String value) { + + public TicketId { + if (value == null) { + throw new ValidationException("Invalid value for TicketId"); + } + } + + public static TicketId unique() { + return new TicketId(UUID.randomUUID().toString()); + } + + public static TicketId with(final String value) { + try { + return new TicketId(UUID.fromString(value).toString()); + } catch (IllegalArgumentException ex) { + throw new ValidationException("Invalid value for TicketId"); + } + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketRepository.java b/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketRepository.java new file mode 100644 index 00000000..9150508b --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketRepository.java @@ -0,0 +1,19 @@ +package br.com.fullcycle.domain.event.ticket; + +import br.com.fullcycle.domain.event.EventId; + +import java.util.List; +import java.util.Optional; + +public interface TicketRepository { + + Optional ticketOfId(TicketId anId); + + Ticket create(Ticket ticket); + + Ticket update(Ticket ticket); + + void deleteAll(); + + List ticketsByEventId(EventId anEventId); +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketStatus.java b/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketStatus.java new file mode 100644 index 00000000..642ed8d0 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/event/ticket/TicketStatus.java @@ -0,0 +1,5 @@ +package br.com.fullcycle.domain.event.ticket; + +public enum TicketStatus { + PENDING, PROCESSING, PAID, CANCELLED; +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/exceptions/ValidationException.java b/domain/src/main/java/br/com/fullcycle/domain/exceptions/ValidationException.java new file mode 100644 index 00000000..66fbf9ec --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/exceptions/ValidationException.java @@ -0,0 +1,12 @@ +package br.com.fullcycle.domain.exceptions; + +public class ValidationException extends RuntimeException { + + public ValidationException(final String message) { + super(message, null, true, false); + } + + public ValidationException(final String message, final Throwable cause) { + super(message, cause, true, false); + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/partner/Partner.java b/domain/src/main/java/br/com/fullcycle/domain/partner/Partner.java new file mode 100644 index 00000000..b617356c --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/partner/Partner.java @@ -0,0 +1,72 @@ +package br.com.fullcycle.domain.partner; + +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.person.Cnpj; +import br.com.fullcycle.domain.person.Email; +import br.com.fullcycle.domain.person.Name; + +import java.util.Objects; + +public class Partner { + + private final PartnerId partnerId; + private Name name; + private Cnpj cnpj; + private Email email; + + public Partner(final PartnerId partnerId, final String name, final String cnpj, final String email) { + if (partnerId == null) { + throw new ValidationException("Invalid partnerId for Partner"); + } + + this.partnerId = partnerId; + this.setName(name); + this.setCnpj(cnpj); + this.setEmail(email); + } + + public static Partner newPartner(String name, String cnpj, String email) { + return new Partner(PartnerId.unique(), name, cnpj, email); + } + + public PartnerId partnerId() { + return partnerId; + } + + public Name name() { + return name; + } + + public Cnpj cnpj() { + return cnpj; + } + + public Email email() { + return email; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Partner partner = (Partner) o; + return Objects.equals(partnerId, partner.partnerId); + } + + @Override + public int hashCode() { + return Objects.hash(partnerId); + } + + private void setCnpj(final String cnpj) { + this.cnpj = new Cnpj(cnpj); + } + + private void setEmail(final String email) { + this.email = new Email(email); + } + + private void setName(final String name) { + this.name = new Name(name); + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/partner/PartnerId.java b/domain/src/main/java/br/com/fullcycle/domain/partner/PartnerId.java new file mode 100644 index 00000000..e7505389 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/partner/PartnerId.java @@ -0,0 +1,26 @@ +package br.com.fullcycle.domain.partner; + +import br.com.fullcycle.domain.exceptions.ValidationException; + +import java.util.UUID; + +public record PartnerId(String value) { + + public PartnerId { + if (value == null) { + throw new ValidationException("Invalid value for PartnerId"); + } + } + + public static PartnerId unique() { + return new PartnerId(UUID.randomUUID().toString()); + } + + public static PartnerId with(final String value) { + try { + return new PartnerId(UUID.fromString(value).toString()); + } catch (IllegalArgumentException ex) { + throw new ValidationException("Invalid value for PartnerId"); + } + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/partner/PartnerRepository.java b/domain/src/main/java/br/com/fullcycle/domain/partner/PartnerRepository.java new file mode 100644 index 00000000..c16fe0fd --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/partner/PartnerRepository.java @@ -0,0 +1,21 @@ +package br.com.fullcycle.domain.partner; + +import br.com.fullcycle.domain.person.Cnpj; +import br.com.fullcycle.domain.person.Email; + +import java.util.Optional; + +public interface PartnerRepository { + + Optional partnerOfId(PartnerId anId); + + Optional partnerOfCNPJ(Cnpj cnpj); + + Optional partnerOfEmail(Email email); + + Partner create(Partner partner); + + Partner update(Partner partner); + + void deleteAll(); +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/person/Cnpj.java b/domain/src/main/java/br/com/fullcycle/domain/person/Cnpj.java new file mode 100644 index 00000000..443338fd --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/person/Cnpj.java @@ -0,0 +1,12 @@ +package br.com.fullcycle.domain.person; + +import br.com.fullcycle.domain.exceptions.ValidationException; + +public record Cnpj(String value) { + + public Cnpj { + if (value == null || !value.matches("^\\d{2}\\.\\d{3}\\.\\d{3}\\/\\d{4}\\-\\d{2}$")) { + throw new ValidationException("Invalid value for Cnpj"); + } + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/person/Cpf.java b/domain/src/main/java/br/com/fullcycle/domain/person/Cpf.java new file mode 100644 index 00000000..01c264c6 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/person/Cpf.java @@ -0,0 +1,12 @@ +package br.com.fullcycle.domain.person; + +import br.com.fullcycle.domain.exceptions.ValidationException; + +public record Cpf(String value) { + + public Cpf { + if (value == null || !value.matches("^\\d{3}\\.\\d{3}\\.\\d{3}\\-\\d{2}$")) { + throw new ValidationException("Invalid value for Cpf"); + } + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/person/Email.java b/domain/src/main/java/br/com/fullcycle/domain/person/Email.java new file mode 100644 index 00000000..bac5f1e7 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/person/Email.java @@ -0,0 +1,12 @@ +package br.com.fullcycle.domain.person; + +import br.com.fullcycle.domain.exceptions.ValidationException; + +public record Email(String value) { + + public Email { + if (value == null || !value.matches("^\\w+([\\.-]?\\w+)*@\\w+([\\.-]?\\w+)*(\\.\\w{2,3})+$")) { + throw new ValidationException("Invalid value for Email"); + } + } +} diff --git a/domain/src/main/java/br/com/fullcycle/domain/person/Name.java b/domain/src/main/java/br/com/fullcycle/domain/person/Name.java new file mode 100644 index 00000000..ee523211 --- /dev/null +++ b/domain/src/main/java/br/com/fullcycle/domain/person/Name.java @@ -0,0 +1,12 @@ +package br.com.fullcycle.domain.person; + +import br.com.fullcycle.domain.exceptions.ValidationException; + +public record Name(String value) { + + public Name { + if (value == null) { + throw new ValidationException("Invalid value for Name"); + } + } +} diff --git a/domain/src/test/java/br/com/fullcycle/domain/customer/CustomerTest.java b/domain/src/test/java/br/com/fullcycle/domain/customer/CustomerTest.java new file mode 100644 index 00000000..fd466cf8 --- /dev/null +++ b/domain/src/test/java/br/com/fullcycle/domain/customer/CustomerTest.java @@ -0,0 +1,76 @@ +package br.com.fullcycle.domain.customer; + +import br.com.fullcycle.domain.exceptions.ValidationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class CustomerTest { + + @Test + @DisplayName("Deve instanciar um cliente") + public void testCreateCustomer() { + // given + final var expectedCPF = "123.456.789-01"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + + // when + final var actualCustomer = Customer.newCustomer(expectedName, expectedCPF, expectedEmail); + + // then + Assertions.assertNotNull(actualCustomer.customerId()); + Assertions.assertEquals(expectedCPF, actualCustomer.cpf().value()); + Assertions.assertEquals(expectedEmail, actualCustomer.email().value()); + Assertions.assertEquals(expectedName, actualCustomer.name().value()); + } + + @Test + @DisplayName("Não deve instanciar um cliente com CPF invalido") + public void testCreateCustomerWithInvalidCPF() { + // given + final var expectedError = "Invalid value for Cpf"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> Customer.newCustomer("John Doe", "123456.789-01", "john.doe@gmail.com") + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + @Test + @DisplayName("Não deve instanciar um cliente com nome invalido") + public void testCreateCustomerWithInvalidName() { + // given + final var expectedError = "Invalid value for Name"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> Customer.newCustomer(null, "123.456.789-01", "john.doe@gmail.com") + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + + @Test + @DisplayName("Não deve instanciar um cliente com email invalido") + public void testCreateCustomerWithInvalidEmail() { + // given + final var expectedError = "Invalid value for Email"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> Customer.newCustomer("John Doe", "123.456.789-01", "john.doe@gmail") + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } +} diff --git a/domain/src/test/java/br/com/fullcycle/domain/event/EventTest.java b/domain/src/test/java/br/com/fullcycle/domain/event/EventTest.java new file mode 100644 index 00000000..aa3f3bfa --- /dev/null +++ b/domain/src/test/java/br/com/fullcycle/domain/event/EventTest.java @@ -0,0 +1,252 @@ +package br.com.fullcycle.domain.event; + +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.partner.Partner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.format.DateTimeFormatter; + +public class EventTest { + + @Test + @DisplayName("Deve criar um evento") + public void testCreateEvent() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var expectedDate = "2021-01-01"; + final var expectedName = "Disney on Ice"; + final var expectedTotalSpots = 10; + final var expectedPartnerId = aPartner.partnerId().value(); + final var expectedTickets = 0; + + // when + final var actualEvent = Event.newEvent(expectedName, expectedDate, expectedTotalSpots, aPartner); + + // then + Assertions.assertNotNull(actualEvent.eventId()); + Assertions.assertEquals(expectedDate, actualEvent.date().format(DateTimeFormatter.ISO_LOCAL_DATE)); + Assertions.assertEquals(expectedName, actualEvent.name().value()); + Assertions.assertEquals(expectedTotalSpots, actualEvent.totalSpots()); + Assertions.assertEquals(expectedPartnerId, actualEvent.partnerId().value()); + Assertions.assertEquals(expectedTickets, actualEvent.allTickets().size()); + } + + @Test + @DisplayName("Não deve criar um evento com nome inválido") + public void testCreateEventWithInvalidName() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var expectedError = "Invalid value for Name"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> Event.newEvent(null, "2021-01-01", 10, aPartner) + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + @Test + @DisplayName("Não deve criar um evento com data inválido") + public void testCreateEventWithInvalidDate() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var expectedError = "Invalid date for Event"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> Event.newEvent("Disney", "20210101", 10, aPartner) + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + @Test + @DisplayName("Deve reservar um ticket quando é possível") + public void testReserveTicket() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var aCustomer = + Customer.newCustomer("John Doe", "123.456.789-01", "john.doe@gmail.com"); + + final var expectedCustomerId = aCustomer.customerId(); + final var expectedDate = "2021-01-01"; + final var expectedName = "Disney on Ice"; + final var expectedTotalSpots = 10; + final var expectedPartnerId = aPartner.partnerId().value(); + final var expectedTickets = 1; + final var expectedTicketOrder = 1; + final var expectedDomainEvent = "event-ticket.reserved"; + + final var actualEvent = Event.newEvent(expectedName, expectedDate, expectedTotalSpots, aPartner); + + final var expectedEventId = actualEvent.eventId(); + + // when + final var actualTicket = actualEvent.reserveTicket(aCustomer.customerId()); + + // then + Assertions.assertNotNull(actualTicket.eventTicketId()); + Assertions.assertNull(actualTicket.ticketId()); + Assertions.assertEquals(expectedEventId, actualTicket.eventId()); + Assertions.assertEquals(expectedCustomerId, actualTicket.customerId()); + + Assertions.assertEquals(expectedDate, actualEvent.date().format(DateTimeFormatter.ISO_LOCAL_DATE)); + Assertions.assertEquals(expectedName, actualEvent.name().value()); + Assertions.assertEquals(expectedTotalSpots, actualEvent.totalSpots()); + Assertions.assertEquals(expectedPartnerId, actualEvent.partnerId().value()); + Assertions.assertEquals(expectedTickets, actualEvent.allTickets().size()); + + final var actualEventTicket = actualEvent.allTickets().iterator().next(); + Assertions.assertEquals(expectedTicketOrder, actualEventTicket.ordering()); + Assertions.assertEquals(expectedEventId, actualEventTicket.eventId()); + Assertions.assertEquals(expectedCustomerId, actualEventTicket.customerId()); + Assertions.assertEquals(actualTicket.ticketId(), actualEventTicket.ticketId()); + + final var actualDomainEvents = actualEvent.allDomainEvents().iterator().next(); + Assertions.assertEquals(expectedDomainEvent, actualDomainEvents.type()); + } + + @Test + @DisplayName("Não deve reservar um ticket quando o evento está esgotado") + public void testReserveTicketWhenEventIsSoldOut() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var aCustomer = + Customer.newCustomer("John Doe", "123.456.789-01", "john.doe@gmail.com"); + + final var aCustomer2 = + Customer.newCustomer("John1 Doe", "111.456.789-01", "john1.doe@gmail.com"); + + + final var expectedTotalSpots = 1; + final var expectedError = "Event sold out"; + + final var actualEvent = Event.newEvent("Disney on Ice", "2021-01-01", expectedTotalSpots, aPartner); + + actualEvent.reserveTicket(aCustomer.customerId()); + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> actualEvent.reserveTicket(aCustomer2.customerId()) + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + @Test + @DisplayName("Não deve reservar dois tickets para um mesmo cliente") + public void testReserveTwoTicketsForTheSameClient() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var aCustomer = + Customer.newCustomer("John Doe", "123.456.789-01", "john.doe@gmail.com"); + + final var expectedTotalSpots = 1; + final var expectedError = "Email already registered"; + + final var actualEvent = Event.newEvent("Disney on Ice", "2021-01-01", expectedTotalSpots, aPartner); + + actualEvent.reserveTicket(aCustomer.customerId()); + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> actualEvent.reserveTicket(aCustomer.customerId()) + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + @Test + @DisplayName("Deve cancelar um evento ativo") + public void testCancelEvent() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var expectedDomainEvent = "event.cancelled"; + + final var actualEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + + // when + actualEvent.cancel(); + + // then + Assertions.assertEquals(EventStatus.CANCELLED, actualEvent.status()); + + final var actualDomainEvents = actualEvent.allDomainEvents().iterator().next(); + Assertions.assertEquals(expectedDomainEvent, actualDomainEvents.type()); + } + + @Test + @DisplayName("Não deve cancelar um evento já cancelado") + public void testCancelEventTwice() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var expectedError = "Event already cancelled"; + + final var actualEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + + actualEvent.cancel(); + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + actualEvent::cancel + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + @Test + @DisplayName("Não deve reservar um ticket em um evento cancelado") + public void testReserveTicketOnCancelledEvent() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var aCustomer = + Customer.newCustomer("John Doe", "123.456.789-01", "john.doe@gmail.com"); + + final var expectedError = "Event is cancelled"; + + final var actualEvent = Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + + actualEvent.cancel(); + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> actualEvent.reserveTicket(aCustomer.customerId()) + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } +} diff --git a/domain/src/test/java/br/com/fullcycle/domain/event/ticket/TicketTest.java b/domain/src/test/java/br/com/fullcycle/domain/event/ticket/TicketTest.java new file mode 100644 index 00000000..2f51364f --- /dev/null +++ b/domain/src/test/java/br/com/fullcycle/domain/event/ticket/TicketTest.java @@ -0,0 +1,88 @@ +package br.com.fullcycle.domain.event.ticket; + +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.partner.Partner; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class TicketTest { + + @Test + @DisplayName("Deve criar um ticket") + public void testReserveTicket() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var aCustomer = + Customer.newCustomer("John Doe", "123.456.789-01", "john.doe@gmail.com"); + + final var anEvent = + Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + + final var expectedTickets = 1; + final var expectedTicketOrder = 1; + final var expectedTicketStatus = TicketStatus.PENDING; + final var expectedEventId = anEvent.eventId(); + final var expectedCustomerId = aCustomer.customerId(); + + // when + final var actualTicket = Ticket.newTicket(aCustomer.customerId(), anEvent.eventId()); + + // then + Assertions.assertNotNull(actualTicket.ticketId()); + Assertions.assertNotNull(actualTicket.reservedAt()); + Assertions.assertNull(actualTicket.paidAt()); + Assertions.assertEquals(expectedEventId, actualTicket.eventId()); + Assertions.assertEquals(expectedCustomerId, actualTicket.customerId()); + Assertions.assertEquals(expectedTicketStatus, actualTicket.status()); + } + + @Test + @DisplayName("Deve cancelar um ticket") + public void testCancelTicket() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var aCustomer = + Customer.newCustomer("John Doe", "123.456.789-01", "john.doe@gmail.com"); + + final var anEvent = + Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + + final var actualTicket = Ticket.newTicket(aCustomer.customerId(), anEvent.eventId()); + + // when + actualTicket.cancel(); + + // then + Assertions.assertEquals(TicketStatus.CANCELLED, actualTicket.status()); + } + + @Test + @DisplayName("Deve ser idempotente ao cancelar um ticket já cancelado") + public void testCancelTicketTwice() throws Exception { + // given + final var aPartner = + Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com"); + + final var aCustomer = + Customer.newCustomer("John Doe", "123.456.789-01", "john.doe@gmail.com"); + + final var anEvent = + Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner); + + final var actualTicket = Ticket.newTicket(aCustomer.customerId(), anEvent.eventId()); + + actualTicket.cancel(); + + // when + actualTicket.cancel(); + + // then + Assertions.assertEquals(TicketStatus.CANCELLED, actualTicket.status()); + } +} \ No newline at end of file diff --git a/domain/src/test/java/br/com/fullcycle/domain/partner/PartnerTest.java b/domain/src/test/java/br/com/fullcycle/domain/partner/PartnerTest.java new file mode 100644 index 00000000..79025cc4 --- /dev/null +++ b/domain/src/test/java/br/com/fullcycle/domain/partner/PartnerTest.java @@ -0,0 +1,76 @@ +package br.com.fullcycle.domain.partner; + +import br.com.fullcycle.domain.exceptions.ValidationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +public class PartnerTest { + + @Test + @DisplayName("Deve instanciar um partner") + public void testCreatePartner() { + // given + final var expectedCNPJ = "41.536.538/0001-00"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + + // when + final var actualPartner = Partner.newPartner(expectedName, expectedCNPJ, expectedEmail); + + // then + Assertions.assertNotNull(actualPartner.partnerId()); + Assertions.assertEquals(expectedCNPJ, actualPartner.cnpj().value()); + Assertions.assertEquals(expectedEmail, actualPartner.email().value()); + Assertions.assertEquals(expectedName, actualPartner.name().value()); + } + + @Test + @DisplayName("Não deve instanciar um partner com CNPJ invalido") + public void testCreatePartnerWithInvalidCNPJ() { + // given + final var expectedError = "Invalid value for Cnpj"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> Partner.newPartner("John Doe", "123456.789-01", "john.doe@gmail.com") + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + @Test + @DisplayName("Não deve instanciar um partner com nome invalido") + public void testCreatePartnerWithInvalidName() { + // given + final var expectedError = "Invalid value for Name"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> Partner.newPartner(null, "41.536.538/0001-00", "john.doe@gmail.com") + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + + @Test + @DisplayName("Não deve instanciar um partner com email invalido") + public void testCreatePartnerWithInvalidEmail() { + // given + final var expectedError = "Invalid value for Email"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail") + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } +} diff --git a/domain/src/test/java/br/com/fullcycle/domain/person/CnpjTest.java b/domain/src/test/java/br/com/fullcycle/domain/person/CnpjTest.java new file mode 100644 index 00000000..d5a3d166 --- /dev/null +++ b/domain/src/test/java/br/com/fullcycle/domain/person/CnpjTest.java @@ -0,0 +1,54 @@ +package br.com.fullcycle.domain.person; + +import br.com.fullcycle.domain.exceptions.ValidationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CnpjTest { + + @Test + @DisplayName("Deve instanciar um CNPJ valido") + public void testCreateCNPJ() { + // given + final var expectedCNPJ = "41.536.538/0001-00"; + + // when + final var actualCnpj = new Cnpj(expectedCNPJ); + + // then + Assertions.assertEquals(expectedCNPJ, actualCnpj.value()); + } + + @Test + @DisplayName("Não deve instanciar um CNPJ invalido") + public void testCreateCNPJWithInvalidValue() { + // given + final var expectedError = "Invalid value for Cnpj"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> new Cnpj("123456.789-01") + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + @Test + @DisplayName("Não deve instanciar um CNPJ null") + public void testCreateCNPJWithNullValue() { + // given + final var expectedError = "Invalid value for Cnpj"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> new Cnpj(null) + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } +} \ No newline at end of file diff --git a/domain/src/test/java/br/com/fullcycle/domain/person/CpfTest.java b/domain/src/test/java/br/com/fullcycle/domain/person/CpfTest.java new file mode 100644 index 00000000..1743417a --- /dev/null +++ b/domain/src/test/java/br/com/fullcycle/domain/person/CpfTest.java @@ -0,0 +1,54 @@ +package br.com.fullcycle.domain.person; + +import br.com.fullcycle.domain.exceptions.ValidationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class CpfTest { + + @Test + @DisplayName("Deve instanciar um CPF valido") + public void testCreateCPF() { + // given + final var expectedCPF = "411.536.538-00"; + + // when + final var actualCpf = new Cpf(expectedCPF); + + // then + Assertions.assertEquals(expectedCPF, actualCpf.value()); + } + + @Test + @DisplayName("Não deve instanciar um CPF invalido") + public void testCreateCPFWithInvalidValue() { + // given + final var expectedError = "Invalid value for Cpf"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> new Cpf("123456.789-01") + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + @Test + @DisplayName("Não deve instanciar um CPF null") + public void testCreateCPFWithNullValue() { + // given + final var expectedError = "Invalid value for Cpf"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> new Cpf(null) + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } +} \ No newline at end of file diff --git a/domain/src/test/java/br/com/fullcycle/domain/person/EmailTest.java b/domain/src/test/java/br/com/fullcycle/domain/person/EmailTest.java new file mode 100644 index 00000000..f50c339e --- /dev/null +++ b/domain/src/test/java/br/com/fullcycle/domain/person/EmailTest.java @@ -0,0 +1,54 @@ +package br.com.fullcycle.domain.person; + +import br.com.fullcycle.domain.exceptions.ValidationException; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class EmailTest { + + @Test + @DisplayName("Deve instanciar um Email valido") + public void testCreateEmail() { + // given + final var expectedEmail = "john@gmail.com"; + + // when + final var actualEmail = new Email(expectedEmail); + + // then + Assertions.assertEquals(expectedEmail, actualEmail.value()); + } + + @Test + @DisplayName("Não deve instanciar um Email invalido") + public void testCreateEmailWithInvalidValue() { + // given + final var expectedError = "Invalid value for Email"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> new Email("josh@s") + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } + + @Test + @DisplayName("Não deve instanciar um Email null") + public void testCreateEmailWithNullValue() { + // given + final var expectedError = "Invalid value for Email"; + + // when + final var actualError = Assertions.assertThrows( + ValidationException.class, + () -> new Email(null) + ); + + // then + Assertions.assertEquals(expectedError, actualError.getMessage()); + } +} \ No newline at end of file diff --git a/infrastructure/build.gradle.kts b/infrastructure/build.gradle.kts new file mode 100644 index 00000000..f56aea44 --- /dev/null +++ b/infrastructure/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + java + `java-conventions` + `jacoco-report-aggregation` + id("org.springframework.boot") version "3.1.2" + id("io.spring.dependency-management") version "1.1.2" +} + +group = "br.com.fullcycle.infrastructure" + +tasks.bootJar { + archiveBaseName.set("application") + destinationDirectory.set(file("${rootProject.buildDir}/libs")) +} + +dependencies { + implementation(project(":domain")) + implementation(project(":application")) + + implementation("io.hypersistence:hypersistence-tsid:2.1.0") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-graphql") + implementation("org.springframework.boot:spring-boot-starter-web") + + implementation("jakarta.inject:jakarta.inject-api:2.0.1") + + runtimeOnly("com.mysql:mysql-connector-j") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework:spring-webflux") + testImplementation("org.springframework.graphql:spring-graphql-test") + + testRuntimeOnly("com.h2database:h2") +} + +tasks.testCodeCoverageReport { + reports { + xml.required.set(true) + xml.outputLocation.set(file("$rootDir/build/reports/jacoco/test/jacocoTestReport.xml")) + + html.required.set(true) + html.outputLocation.set(file("$rootDir/build/reports/jacoco/test/")) + } +} + +tasks.named("jacocoTestReport") { + dependsOn(tasks.named("testCodeCoverageReport")) +} \ No newline at end of file diff --git a/src/main/java/br/com/fullcycle/hexagonal/Main.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/Main.java similarity index 66% rename from src/main/java/br/com/fullcycle/hexagonal/Main.java rename to infrastructure/src/main/java/br/com/fullcycle/infrastructure/Main.java index 6bd1948b..36cdd746 100644 --- a/src/main/java/br/com/fullcycle/hexagonal/Main.java +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/Main.java @@ -1,13 +1,14 @@ -package br.com.fullcycle.hexagonal; +package br.com.fullcycle.infrastructure; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; +@EnableScheduling @SpringBootApplication public class Main { public static void main(String[] args) { SpringApplication.run(Main.class, args); } - -} +} \ No newline at end of file diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/ControllerConfig.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/ControllerConfig.java new file mode 100644 index 00000000..330dcb2f --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/ControllerConfig.java @@ -0,0 +1,19 @@ +package br.com.fullcycle.infrastructure.configurations; + +import br.com.fullcycle.application.partner.CreatePartnerUseCase; +import br.com.fullcycle.application.partner.GetPartnerByIdUseCase; +import br.com.fullcycle.infrastructure.rest.PartnerFnController; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class ControllerConfig { + + @Bean + public PartnerFnController partnerFnController( + final CreatePartnerUseCase createPartnerUseCase, + final GetPartnerByIdUseCase getPartnerByIdUseCase + ) { + return new PartnerFnController(createPartnerUseCase, getPartnerByIdUseCase); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/OutboxConfig.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/OutboxConfig.java new file mode 100644 index 00000000..a9c0e7fe --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/OutboxConfig.java @@ -0,0 +1,19 @@ +package br.com.fullcycle.infrastructure.configurations; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +@Configuration +public class OutboxConfig { + + @Bean + public TaskExecutor queueExecutor() { + var executor = new ThreadPoolTaskExecutor(); + executor.setQueueCapacity(200); + executor.setMaxPoolSize(Runtime.getRuntime().availableProcessors() * 4); + executor.setCorePoolSize(2); + return executor; + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/RouterConfig.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/RouterConfig.java new file mode 100644 index 00000000..6c0740de --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/RouterConfig.java @@ -0,0 +1,21 @@ +package br.com.fullcycle.infrastructure.configurations; + +import br.com.fullcycle.infrastructure.http.SpringHttpRouter; +import br.com.fullcycle.infrastructure.rest.PartnerFnController; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.function.RouterFunction; + +// Frameworks and Drivers +@Configuration +public class RouterConfig { + + @Bean + public RouterFunction routes( + final PartnerFnController partnerFnController + ) { + final var router = new SpringHttpRouter(); + partnerFnController.bind(router); + return router.router().build(); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/UseCaseConfig.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/UseCaseConfig.java new file mode 100644 index 00000000..5707cd33 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/configurations/UseCaseConfig.java @@ -0,0 +1,91 @@ +package br.com.fullcycle.infrastructure.configurations; + +import br.com.fullcycle.application.customer.CreateCustomerUseCase; +import br.com.fullcycle.application.customer.GetCustomerByIdUseCase; +import br.com.fullcycle.application.event.CancelEventUseCase; +import br.com.fullcycle.application.event.CreateEventUseCase; +import br.com.fullcycle.application.event.GetEventByIdUseCase; +import br.com.fullcycle.application.event.SubscribeCustomerToEventUseCase; +import br.com.fullcycle.application.partner.CreatePartnerUseCase; +import br.com.fullcycle.application.partner.GetPartnerByIdUseCase; +import br.com.fullcycle.application.ticket.CancelEventTicketsUseCase; +import br.com.fullcycle.application.ticket.CreateTicketForCustomerUseCase; +import br.com.fullcycle.domain.customer.CustomerRepository; +import br.com.fullcycle.domain.event.EventRepository; +import br.com.fullcycle.domain.event.ticket.TicketRepository; +import br.com.fullcycle.domain.partner.PartnerRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Objects; + +@Configuration +public class UseCaseConfig { + + private final CustomerRepository customerRepository; + private final EventRepository eventRepository; + private final PartnerRepository partnerRepository; + private final TicketRepository ticketRepository; + + public UseCaseConfig( + final CustomerRepository customerRepository, + final EventRepository eventRepository, + final PartnerRepository partnerRepository, + final TicketRepository ticketRepository + ) { + this.customerRepository = Objects.requireNonNull(customerRepository); + this.eventRepository = Objects.requireNonNull(eventRepository); + this.partnerRepository = Objects.requireNonNull(partnerRepository); + this.ticketRepository = Objects.requireNonNull(ticketRepository); + } + + @Bean + public CreateCustomerUseCase createCustomerUseCase() { + return new CreateCustomerUseCase(customerRepository); + } + + @Bean + public CreateEventUseCase createEventUseCase() { + return new CreateEventUseCase(eventRepository, partnerRepository); + } + + @Bean + public CreatePartnerUseCase createPartnerUseCase() { + return new CreatePartnerUseCase(partnerRepository); + } + + @Bean + public GetCustomerByIdUseCase getCustomerByIdUseCase() { + return new GetCustomerByIdUseCase(customerRepository); + } + + @Bean + public GetPartnerByIdUseCase getPartnerByIdUseCase() { + return new GetPartnerByIdUseCase(partnerRepository); + } + + @Bean + public SubscribeCustomerToEventUseCase subscribeCustomerToEventUseCase() { + return new SubscribeCustomerToEventUseCase(customerRepository, eventRepository); + } + + @Bean + public CreateTicketForCustomerUseCase createTicketForCustomerUseCase() { + return new CreateTicketForCustomerUseCase(ticketRepository); + } + + @Bean + public CancelEventUseCase cancelEventUseCase() { + return new CancelEventUseCase(eventRepository); + } + + @Bean + public CancelEventTicketsUseCase cancelEventTicketsUseCase() { + return new CancelEventTicketsUseCase(ticketRepository); + } + + @Bean + public GetEventByIdUseCase getEventByIdUseCase() { + return new GetEventByIdUseCase(eventRepository); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/NewCustomerDTO.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/NewCustomerDTO.java new file mode 100644 index 00000000..8397b5fb --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/NewCustomerDTO.java @@ -0,0 +1,5 @@ +package br.com.fullcycle.infrastructure.dtos; + +public record NewCustomerDTO(String cpf, String email, String name) { + +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/NewEventDTO.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/NewEventDTO.java new file mode 100644 index 00000000..0ad47aaf --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/NewEventDTO.java @@ -0,0 +1,10 @@ +package br.com.fullcycle.infrastructure.dtos; + +public record NewEventDTO( + String name, + String date, + Integer totalSpots, + String partnerId +) { + +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/NewPartnerDTO.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/NewPartnerDTO.java new file mode 100644 index 00000000..dbe3e78b --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/NewPartnerDTO.java @@ -0,0 +1,5 @@ +package br.com.fullcycle.infrastructure.dtos; + +public record NewPartnerDTO(String cnpj, String email, String name) { + +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/SubscribeDTO.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/SubscribeDTO.java new file mode 100644 index 00000000..cb0abc36 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/dtos/SubscribeDTO.java @@ -0,0 +1,4 @@ +package br.com.fullcycle.infrastructure.dtos; + +public record SubscribeDTO(String customerId, String eventId) { +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/gateways/ConsumerQueueGateway.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/gateways/ConsumerQueueGateway.java new file mode 100644 index 00000000..6a7a9bcb --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/gateways/ConsumerQueueGateway.java @@ -0,0 +1,56 @@ +package br.com.fullcycle.infrastructure.gateways; + +import br.com.fullcycle.application.ticket.CancelEventTicketsUseCase; +import br.com.fullcycle.application.ticket.CreateTicketForCustomerUseCase; +import br.com.fullcycle.domain.event.EventCancelled; +import br.com.fullcycle.domain.event.EventTicketReserved; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.util.Objects; + +@Component +public class ConsumerQueueGateway implements QueueGateway { + + private final CreateTicketForCustomerUseCase createTicketForCustomerUseCase; + private final CancelEventTicketsUseCase cancelEventTicketsUseCase; + private final ObjectMapper mapper; + + public ConsumerQueueGateway( + final CreateTicketForCustomerUseCase createTicketForCustomerUseCase, + final CancelEventTicketsUseCase cancelEventTicketsUseCase, + final ObjectMapper mapper + ) { + this.createTicketForCustomerUseCase = Objects.requireNonNull(createTicketForCustomerUseCase); + this.cancelEventTicketsUseCase = Objects.requireNonNull(cancelEventTicketsUseCase); + this.mapper = Objects.requireNonNull(mapper); + } + + @Async(value = "queueExecutor") + @Override + public void publish(final String content) { + if (content == null) { + return; + } + + if (content.contains("event-ticket.reserved")) { + final var dto = safeRead(content, EventTicketReserved.class); + this.createTicketForCustomerUseCase.execute(new CreateTicketForCustomerUseCase.Input(dto.eventTicketId(), dto.eventId(), dto.customerId())); + } + + if (content.contains("event.cancelled")) { + final var dto = safeRead(content, EventCancelled.class); + this.cancelEventTicketsUseCase.execute(new CancelEventTicketsUseCase.Input(dto.eventId())); + } + } + + private T safeRead(final String content, final Class tClass) { + try { + return this.mapper.readValue(content, tClass); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/gateways/QueueGateway.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/gateways/QueueGateway.java new file mode 100644 index 00000000..f6ba0e88 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/gateways/QueueGateway.java @@ -0,0 +1,5 @@ +package br.com.fullcycle.infrastructure.gateways; + +public interface QueueGateway { + void publish(String content); +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/graphql/CustomerResolver.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/graphql/CustomerResolver.java new file mode 100644 index 00000000..083242de --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/graphql/CustomerResolver.java @@ -0,0 +1,37 @@ +package br.com.fullcycle.infrastructure.graphql; + +import br.com.fullcycle.application.customer.CreateCustomerUseCase; +import br.com.fullcycle.application.customer.GetCustomerByIdUseCase; +import br.com.fullcycle.infrastructure.dtos.NewCustomerDTO; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; + +import java.util.Objects; + +// Adapter +@Controller +public class CustomerResolver { + + private final CreateCustomerUseCase createCustomerUseCase; + private final GetCustomerByIdUseCase getCustomerByIdUseCase; + + public CustomerResolver( + final CreateCustomerUseCase createCustomerUseCase, + final GetCustomerByIdUseCase getCustomerByIdUseCase + ) { + this.createCustomerUseCase = Objects.requireNonNull(createCustomerUseCase); + this.getCustomerByIdUseCase = Objects.requireNonNull(getCustomerByIdUseCase); + } + + @MutationMapping + public CreateCustomerUseCase.Output createCustomer(@Argument NewCustomerDTO input) { + return createCustomerUseCase.execute(new CreateCustomerUseCase.Input(input.cpf(), input.email(), input.name())); + } + + @QueryMapping + public GetCustomerByIdUseCase.Output customerOfId(@Argument String id) { + return getCustomerByIdUseCase.execute(new GetCustomerByIdUseCase.Input(id)).orElse(null); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/graphql/EventResolver.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/graphql/EventResolver.java new file mode 100644 index 00000000..92d2a33c --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/graphql/EventResolver.java @@ -0,0 +1,57 @@ +package br.com.fullcycle.infrastructure.graphql; + +import br.com.fullcycle.application.event.CancelEventUseCase; +import br.com.fullcycle.application.event.CreateEventUseCase; +import br.com.fullcycle.application.event.GetEventByIdUseCase; +import br.com.fullcycle.application.event.SubscribeCustomerToEventUseCase; +import br.com.fullcycle.infrastructure.dtos.NewEventDTO; +import br.com.fullcycle.infrastructure.dtos.SubscribeDTO; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; + +@Controller +public class EventResolver { + + private final CreateEventUseCase createEventUseCase; + private final SubscribeCustomerToEventUseCase subscribeCustomerToEventUseCase; + private final CancelEventUseCase cancelEventUseCase; + private final GetEventByIdUseCase getEventByIdUseCase; + + public EventResolver( + final CreateEventUseCase createEventUseCase, + final SubscribeCustomerToEventUseCase subscribeCustomerToEventUseCase, + final CancelEventUseCase cancelEventUseCase, + final GetEventByIdUseCase getEventByIdUseCase + ) { + this.createEventUseCase = Objects.requireNonNull(createEventUseCase); + this.subscribeCustomerToEventUseCase = Objects.requireNonNull(subscribeCustomerToEventUseCase); + this.cancelEventUseCase = Objects.requireNonNull(cancelEventUseCase); + this.getEventByIdUseCase = Objects.requireNonNull(getEventByIdUseCase); + } + + @MutationMapping + public CreateEventUseCase.Output createEvent(@Argument NewEventDTO input) { + return createEventUseCase.execute(new CreateEventUseCase.Input(input.date(), input.name(), input.partnerId(), input.totalSpots())); + } + + @Transactional + @MutationMapping + public SubscribeCustomerToEventUseCase.Output subscribeCustomerToEvent(@Argument SubscribeDTO input) { + return subscribeCustomerToEventUseCase.execute(new SubscribeCustomerToEventUseCase.Input(input.customerId(), input.eventId())); + } + + @MutationMapping + public CancelEventUseCase.Output cancelEvent(@Argument String id) { + return cancelEventUseCase.execute(new CancelEventUseCase.Input(id)); + } + + @QueryMapping + public GetEventByIdUseCase.Output eventOfId(@Argument String id) { + return getEventByIdUseCase.execute(new GetEventByIdUseCase.Input(id)).orElse(null); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/graphql/PartnerResolver.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/graphql/PartnerResolver.java new file mode 100644 index 00000000..173d7ed5 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/graphql/PartnerResolver.java @@ -0,0 +1,37 @@ +package br.com.fullcycle.infrastructure.graphql; + +import br.com.fullcycle.application.partner.CreatePartnerUseCase; +import br.com.fullcycle.application.partner.GetPartnerByIdUseCase; +import br.com.fullcycle.infrastructure.dtos.NewPartnerDTO; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; + +import java.util.Objects; + +// Adapter +@Controller +public class PartnerResolver { + + private final CreatePartnerUseCase createPartnerUseCase; + private final GetPartnerByIdUseCase getPartnerByIdUseCase; + + public PartnerResolver( + final CreatePartnerUseCase createPartnerUseCase, + final GetPartnerByIdUseCase getPartnerByIdUseCase + ) { + this.createPartnerUseCase = Objects.requireNonNull(createPartnerUseCase); + this.getPartnerByIdUseCase = Objects.requireNonNull(getPartnerByIdUseCase); + } + + @MutationMapping + public CreatePartnerUseCase.Output createPartner(@Argument NewPartnerDTO input) { + return createPartnerUseCase.execute(new CreatePartnerUseCase.Input(input.cnpj(), input.email(), input.name())); + } + + @QueryMapping + public GetPartnerByIdUseCase.Output partnerOfId(@Argument String id) { + return getPartnerByIdUseCase.execute(new GetPartnerByIdUseCase.Input(id)).orElse(null); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/http/HttpRouter.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/http/HttpRouter.java new file mode 100644 index 00000000..e27950ef --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/http/HttpRouter.java @@ -0,0 +1,69 @@ +package br.com.fullcycle.infrastructure.http; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +public interface HttpRouter { + + HttpRouter POST(String pattern, HttpHandler handler); + + HttpRouter GET(String pattern, HttpHandler handler); + + interface HttpHandler { + HttpResponse handle(HttpRequest req); + } + + interface HttpRequest { + + T body(Class tClass); + + String pathParam(String name); + + Optional queryParam(String name); + } + + record HttpResponse(int statusCode, Map headers, T body) { + + public static HttpResponse.Builder created(final URI uri) { + return new Builder(201).location(uri); + } + + public static HttpResponse.Builder notFound() { + return new Builder(404); + } + + public static HttpResponse.Builder unprocessableEntity() { + return new Builder(422); + } + + public static HttpResponse ok(final T body) { + return new HttpResponse<>(200, Map.of(), body); + } + + public static class Builder { + + private final int statusCode; + private final Map headers; + + public Builder(int statusCode) { + this.statusCode = statusCode; + this.headers = new HashMap<>(2); + } + + public HttpResponse build() { + return new HttpResponse<>(statusCode, headers, null); + } + + public HttpResponse body(T body) { + return new HttpResponse<>(statusCode, headers, body); + } + + public Builder location(URI uri) { + this.headers.put("Location", uri.toASCIIString()); + return this; + } + } + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/http/SpringHttpRouter.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/http/SpringHttpRouter.java new file mode 100644 index 00000000..2918bf8e --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/http/SpringHttpRouter.java @@ -0,0 +1,73 @@ +package br.com.fullcycle.infrastructure.http; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.servlet.function.HandlerFunction; +import org.springframework.web.servlet.function.RouterFunctions; +import org.springframework.web.servlet.function.ServerRequest; +import org.springframework.web.servlet.function.ServerResponse; + +import java.util.Optional; + +public class SpringHttpRouter implements HttpRouter { + + private static final Logger LOG = LoggerFactory.getLogger(SpringHttpRouter.class); + + private final RouterFunctions.Builder router; + + public SpringHttpRouter() { + this.router = RouterFunctions.route(); + } + + public RouterFunctions.Builder router() { + return router; + } + + @Override + public HttpRouter POST(String pattern, HttpHandler handler) { + this.router.POST(pattern, wrapHandler(pattern, handler)); + return this; + } + + @Override + public HttpRouter GET(String pattern, HttpHandler handler) { + this.router.GET(pattern, wrapHandler(pattern, handler)); + return this; + } + + private static HandlerFunction wrapHandler(String pattern, HttpHandler handler) { + return req -> { + try { + var res = handler.handle(new SpringHttpRequest(req)); + return ServerResponse.status(res.statusCode()) + .headers(headers -> res.headers().forEach(headers::add)) + .body(res.body()); + } catch (final Throwable t) { + LOG.error("Unexpected error was observed at %s".formatted(pattern), t); + return ServerResponse.status(500).body("Unexpected error was observed."); + } + }; + } + + public record SpringHttpRequest(ServerRequest request) implements HttpRequest { + + @Override + public T body(final Class tClass) { + try { + return request.body(tClass); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + + @Override + public String pathParam(String name) { + return request.pathVariable(name); + } + + @Override + public Optional queryParam(String name) { + return request.param(name); + } + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/job/OutboxRelay.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/job/OutboxRelay.java new file mode 100644 index 00000000..12ec2ed4 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/job/OutboxRelay.java @@ -0,0 +1,29 @@ +package br.com.fullcycle.infrastructure.job; + +import br.com.fullcycle.infrastructure.gateways.QueueGateway; +import br.com.fullcycle.infrastructure.jpa.repositories.OutboxJpaRepository; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class OutboxRelay { + + private final OutboxJpaRepository outboxJpaRepository; + private final QueueGateway queueGateway; + + public OutboxRelay(final OutboxJpaRepository outboxJpaRepository, final QueueGateway queueGateway) { + this.outboxJpaRepository = outboxJpaRepository; + this.queueGateway = queueGateway; + } + + @Scheduled(fixedRate = 2_000) + @Transactional + void execute() { + this.outboxJpaRepository.findTop100ByPublishedFalse() + .forEach(it -> { + this.queueGateway.publish(it.content()); + this.outboxJpaRepository.save(it.notePublished()); + }); + } +} diff --git a/src/main/java/br/com/fullcycle/hexagonal/models/Customer.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/CustomerEntity.java similarity index 54% rename from src/main/java/br/com/fullcycle/hexagonal/models/Customer.java rename to infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/CustomerEntity.java index fa68c1c4..ceee744e 100644 --- a/src/main/java/br/com/fullcycle/hexagonal/models/Customer.java +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/CustomerEntity.java @@ -1,21 +1,20 @@ -package br.com.fullcycle.hexagonal.models; +package br.com.fullcycle.infrastructure.jpa.entities; +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.customer.CustomerId; import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.Table; import java.util.Objects; +import java.util.UUID; -import static jakarta.persistence.GenerationType.*; - -@Entity +@Entity(name = "Customer") @Table(name = "customers") -public class Customer { +public class CustomerEntity { @Id - @GeneratedValue(strategy = IDENTITY) - private Long id; + private UUID id; private String name; @@ -23,21 +22,34 @@ public class Customer { private String email; - public Customer() { + public CustomerEntity() { } - public Customer(Long id, String name, String cpf, String email) { + public CustomerEntity(UUID id, String name, String cpf, String email) { this.id = id; this.name = name; this.cpf = cpf; this.email = email; } - public Long getId() { + public static CustomerEntity of(final Customer customer) { + return new CustomerEntity( + UUID.fromString(customer.customerId().value()), + customer.name().value(), + customer.cpf().value(), + customer.email().value() + ); + } + + public Customer toCustomer() { + return new Customer(CustomerId.with(this.id.toString()), this.name, this.cpf, this.email); + } + + public UUID getId() { return id; } - public void setId(Long id) { + public void setId(UUID id) { this.id = id; } @@ -69,7 +81,7 @@ public void setEmail(String email) { public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - Customer customer = (Customer) o; + CustomerEntity customer = (CustomerEntity) o; return Objects.equals(id, customer.id); } diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/EventEntity.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/EventEntity.java new file mode 100644 index 00000000..cd105ecc --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/EventEntity.java @@ -0,0 +1,152 @@ +package br.com.fullcycle.infrastructure.jpa.entities; + +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.event.EventStatus; +import br.com.fullcycle.domain.event.EventTicket; +import jakarta.persistence.*; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Entity(name = "Event") +@Table(name = "events") +public class EventEntity { + + @Id + private UUID id; + + private String name; + + private LocalDate date; + + private int totalSpots; + + private UUID partnerId; + + @Enumerated(EnumType.STRING) + private EventStatus status; + + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "event") + private Set tickets; + + public EventEntity() { + this.tickets = new HashSet<>(); + } + + public EventEntity(UUID id, String name, LocalDate date, int totalSpots, UUID partnerId, EventStatus status) { + this(); + this.id = id; + this.name = name; + this.date = date; + this.totalSpots = totalSpots; + this.partnerId = partnerId; + this.status = status; + } + + public static EventEntity of(final Event event) { + final var entity = new EventEntity( + UUID.fromString(event.eventId().value()), + event.name().value(), + event.date(), + event.totalSpots(), + UUID.fromString(event.partnerId().value()), + event.status() + ); + + event.allTickets().forEach(entity::addTicket); + + return entity; + } + + public Event toEvent() { + return Event.restore( + this.id().toString(), + this.name(), + this.date().format(DateTimeFormatter.ISO_LOCAL_DATE), + this.totalSpots(), + this.partnerId().toString(), + this.status().name(), + this.tickets().stream() + .map(EventTicketEntity::toEventTicket) + .collect(Collectors.toSet()) + ); + } + + private void addTicket(final EventTicket ticket) { + this.tickets.add(EventTicketEntity.of(this, ticket)); + } + + public UUID id() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String name() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public LocalDate date() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public int totalSpots() { + return totalSpots; + } + + public void setTotalSpots(int totalSpots) { + this.totalSpots = totalSpots; + } + + public UUID partnerId() { + return partnerId; + } + + public void setPartnerId(UUID partnerId) { + this.partnerId = partnerId; + } + + public EventStatus status() { + return status; + } + + public void setStatus(EventStatus status) { + this.status = status; + } + + public Set tickets() { + return tickets; + } + + public void setTickets(Set tickets) { + this.tickets = tickets; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EventEntity event = (EventEntity) o; + return Objects.equals(id, event.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/EventTicketEntity.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/EventTicketEntity.java new file mode 100644 index 00000000..22028f4c --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/EventTicketEntity.java @@ -0,0 +1,118 @@ +package br.com.fullcycle.infrastructure.jpa.entities; + +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.EventTicket; +import br.com.fullcycle.domain.event.EventTicketId; +import br.com.fullcycle.domain.event.ticket.TicketId; +import jakarta.persistence.*; + +import java.util.Objects; +import java.util.UUID; + +@Entity(name = "EventTicket") +@Table(name = "events_tickets") +public class EventTicketEntity { + + @Id + private UUID eventTicketId; + + private UUID ticketId; + + private UUID customerId; + + private int ordering; + + @ManyToOne(fetch = FetchType.LAZY) + private EventEntity event; + + public EventTicketEntity() { + } + + public EventTicketEntity( + final UUID eventTicketId, + final UUID customerId, + final int ordering, + final UUID ticketId, + final EventEntity event + ) { + this.eventTicketId = eventTicketId; + this.ticketId = ticketId; + this.customerId = customerId; + this.event = event; + this.ordering = ordering; + } + + public static EventTicketEntity of(final EventEntity event, final EventTicket ev) { + return new EventTicketEntity( + UUID.fromString(ev.eventTicketId().value()), + UUID.fromString(ev.customerId().value()), + ev.ordering(), + ev.ticketId() != null ? UUID.fromString(ev.ticketId().value()) : null, + event + ); + } + + public EventTicket toEventTicket() { + return new EventTicket( + EventTicketId.with(eventTicketId.toString()), + EventId.with(this.event.id().toString()), + CustomerId.with(this.customerId.toString()), + this.ticketId != null ? TicketId.with(this.ticketId.toString()) : null, + this.ordering + ); + } + + public UUID eventTicketId() { + return eventTicketId; + } + + public void setEventTicketId(UUID eventTicketId) { + this.eventTicketId = eventTicketId; + } + + public UUID ticketId() { + return ticketId; + } + + public void setTicketId(UUID ticketId) { + this.ticketId = ticketId; + } + + public UUID customerId() { + return customerId; + } + + public void setCustomerId(UUID customerId) { + this.customerId = customerId; + } + + public int ordering() { + return ordering; + } + + public void setOrdering(int ordering) { + this.ordering = ordering; + } + + public EventEntity event() { + return event; + } + + public void setEvent(EventEntity event) { + this.event = event; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EventTicketEntity that = (EventTicketEntity) o; + return Objects.equals(eventTicketId, that.eventTicketId); + } + + @Override + public int hashCode() { + return Objects.hash(eventTicketId); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/OutboxEntity.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/OutboxEntity.java new file mode 100644 index 00000000..a32ffef9 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/OutboxEntity.java @@ -0,0 +1,83 @@ +package br.com.fullcycle.infrastructure.jpa.entities; + +import br.com.fullcycle.domain.DomainEvent; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.util.Objects; +import java.util.UUID; +import java.util.function.Function; + +@Entity(name = "Outbox") +@Table(name = "outbox") +public class OutboxEntity { + + @Id + private UUID id; + + @Column(columnDefinition = "JSON", length = 4_000) + private String content; + + private boolean published; + + public OutboxEntity() { + } + + public OutboxEntity(UUID id, String content, boolean published) { + this.id = id; + this.content = content; + this.published = published; + } + + public static OutboxEntity of(final DomainEvent domainEvent, final Function toJson) { + return new OutboxEntity( + UUID.fromString(domainEvent.domainEventId()), + toJson.apply(domainEvent), + false + ); + } + + public UUID id() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String content() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public boolean published() { + return published; + } + + public void setPublished(boolean published) { + this.published = published; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OutboxEntity that = (OutboxEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public OutboxEntity notePublished() { + this.published = true; + return this; + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/PartnerEntity.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/PartnerEntity.java new file mode 100644 index 00000000..4df8d2c4 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/PartnerEntity.java @@ -0,0 +1,78 @@ +package br.com.fullcycle.infrastructure.jpa.entities; + +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.partner.PartnerId; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import java.util.UUID; + +@Entity(name = "Partner") +@Table(name = "partners") +public class PartnerEntity { + + @Id + private UUID id; + + private String name; + + private String cnpj; + + private String email; + + public PartnerEntity() { + } + + public PartnerEntity(UUID id, String name, String cnpj, String email) { + this.id = id; + this.name = name; + this.cnpj = cnpj; + this.email = email; + } + + public static PartnerEntity of(Partner partner) { + return new PartnerEntity( + UUID.fromString(partner.partnerId().value()), + partner.name().value(), + partner.cnpj().value(), + partner.email().value() + ); + } + + public Partner toPartner() { + return new Partner(PartnerId.with(this.id.toString()), this.name, this.cnpj, this.email); + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getCnpj() { + return cnpj; + } + + public void setCnpj(String cnpj) { + this.cnpj = cnpj; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/TicketEntity.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/TicketEntity.java new file mode 100644 index 00000000..1a779195 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/entities/TicketEntity.java @@ -0,0 +1,133 @@ +package br.com.fullcycle.infrastructure.jpa.entities; + +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.ticket.Ticket; +import br.com.fullcycle.domain.event.ticket.TicketId; +import br.com.fullcycle.domain.event.ticket.TicketStatus; +import jakarta.persistence.*; + +import java.time.Instant; +import java.util.Objects; +import java.util.UUID; + +@Entity(name = "Ticket") +@Table(name = "tickets") +public class TicketEntity { + + @Id + private UUID id; + + private UUID customerId; + + private UUID eventId; + + @Enumerated(EnumType.STRING) + private TicketStatus status; + + private Instant paidAt; + + private Instant reservedAt; + + public TicketEntity() { + } + + public TicketEntity( + final UUID id, + final UUID customerId, + final UUID eventId, + final TicketStatus status, + final Instant paidAt, + final Instant reservedAt + ) { + this.id = id; + this.customerId = customerId; + this.eventId = eventId; + this.status = status; + this.paidAt = paidAt; + this.reservedAt = reservedAt; + } + + public static TicketEntity of(final Ticket ticket) { + return new TicketEntity( + UUID.fromString(ticket.ticketId().value()), + UUID.fromString(ticket.customerId().value()), + UUID.fromString(ticket.eventId().value()), + ticket.status(), + ticket.paidAt(), + ticket.reservedAt() + ); + } + + public Ticket toTicket() { + return new Ticket( + TicketId.with(this.id.toString()), + CustomerId.with(this.customerId.toString()), + EventId.with(this.eventId.toString()), + this.status, + this.paidAt, + this.reservedAt + ); + } + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public UUID customerId() { + return customerId; + } + + public void setCustomerId(UUID customerId) { + this.customerId = customerId; + } + + public UUID eventId() { + return eventId; + } + + public void setEventId(UUID eventId) { + this.eventId = eventId; + } + + public TicketStatus getStatus() { + return status; + } + + public void setStatus(TicketStatus status) { + this.status = status; + } + + public Instant getPaidAt() { + return paidAt; + } + + public void setPaidAt(Instant paidAt) { + this.paidAt = paidAt; + } + + public Instant getReservedAt() { + return reservedAt; + } + + public void setReservedAt(Instant reservedAt) { + this.reservedAt = reservedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TicketEntity that = (TicketEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/CustomerJpaRepository.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/CustomerJpaRepository.java new file mode 100644 index 00000000..d0acb9be --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/CustomerJpaRepository.java @@ -0,0 +1,14 @@ +package br.com.fullcycle.infrastructure.jpa.repositories; + +import br.com.fullcycle.infrastructure.jpa.entities.CustomerEntity; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface CustomerJpaRepository extends CrudRepository { + + Optional findByCpf(String cpf); + + Optional findByEmail(String email); +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/EventJpaRepository.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/EventJpaRepository.java new file mode 100644 index 00000000..7d70fe10 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/EventJpaRepository.java @@ -0,0 +1,10 @@ +package br.com.fullcycle.infrastructure.jpa.repositories; + +import br.com.fullcycle.infrastructure.jpa.entities.EventEntity; +import org.springframework.data.repository.CrudRepository; + +import java.util.UUID; + +public interface EventJpaRepository extends CrudRepository { + +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/OutboxJpaRepository.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/OutboxJpaRepository.java new file mode 100644 index 00000000..e8240f34 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/OutboxJpaRepository.java @@ -0,0 +1,20 @@ +package br.com.fullcycle.infrastructure.jpa.repositories; + +import br.com.fullcycle.infrastructure.jpa.entities.OutboxEntity; +import jakarta.persistence.LockModeType; +import jakarta.persistence.QueryHint; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.repository.CrudRepository; + +import java.util.List; +import java.util.UUID; + +public interface OutboxJpaRepository extends CrudRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints({ + @QueryHint(name = "jakarta.persistence.lock.timeout", value = "2") + }) + List findTop100ByPublishedFalse(); +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/PartnerJpaRepository.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/PartnerJpaRepository.java new file mode 100644 index 00000000..e626bd5c --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/PartnerJpaRepository.java @@ -0,0 +1,14 @@ +package br.com.fullcycle.infrastructure.jpa.repositories; + +import br.com.fullcycle.infrastructure.jpa.entities.PartnerEntity; +import org.springframework.data.repository.CrudRepository; + +import java.util.Optional; +import java.util.UUID; + +public interface PartnerJpaRepository extends CrudRepository { + + Optional findByCnpj(String cnpj); + + Optional findByEmail(String email); +} \ No newline at end of file diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/TicketJpaRepository.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/TicketJpaRepository.java new file mode 100644 index 00000000..f53c7246 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/jpa/repositories/TicketJpaRepository.java @@ -0,0 +1,12 @@ +package br.com.fullcycle.infrastructure.jpa.repositories; + +import br.com.fullcycle.infrastructure.jpa.entities.TicketEntity; +import org.springframework.data.repository.CrudRepository; + +import java.util.List; +import java.util.UUID; + +public interface TicketJpaRepository extends CrudRepository { + + List findAllByEventId(UUID eventId); +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/CustomerDatabaseRepository.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/CustomerDatabaseRepository.java new file mode 100644 index 00000000..918db5d4 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/CustomerDatabaseRepository.java @@ -0,0 +1,66 @@ +package br.com.fullcycle.infrastructure.repositories; + +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.customer.CustomerId; +import br.com.fullcycle.domain.person.Cpf; +import br.com.fullcycle.domain.person.Email; +import br.com.fullcycle.domain.customer.CustomerRepository; +import br.com.fullcycle.infrastructure.jpa.entities.CustomerEntity; +import br.com.fullcycle.infrastructure.jpa.repositories.CustomerJpaRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +// Interface Adapter +@Component +public class CustomerDatabaseRepository implements CustomerRepository { + + private final CustomerJpaRepository customerJpaRepository; + + public CustomerDatabaseRepository(final CustomerJpaRepository customerJpaRepository) { + this.customerJpaRepository = Objects.requireNonNull(customerJpaRepository); + } + + @Override + public Optional customerOfId(final CustomerId anId) { + Objects.requireNonNull(anId, "id cannot be null"); + return this.customerJpaRepository.findById(UUID.fromString(anId.value())) + .map(CustomerEntity::toCustomer); + } + + @Override + public Optional customerOfCPF(final Cpf cpf) { + Objects.requireNonNull(cpf, "Cpf cannot be null"); + return this.customerJpaRepository.findByCpf(cpf.value()) + .map(CustomerEntity::toCustomer); + } + + @Override + public Optional customerOfEmail(final Email email) { + Objects.requireNonNull(email, "Email cannot be null"); + return this.customerJpaRepository.findByEmail(email.value()) + .map(CustomerEntity::toCustomer); + } + + @Override + @Transactional + public Customer create(final Customer customer) { + return this.customerJpaRepository.save(CustomerEntity.of(customer)) + .toCustomer(); + } + + @Override + @Transactional + public Customer update(Customer customer) { + return this.customerJpaRepository.save(CustomerEntity.of(customer)) + .toCustomer(); + } + + @Override + public void deleteAll() { + this.customerJpaRepository.deleteAll(); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/EventDatabaseRepository.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/EventDatabaseRepository.java new file mode 100644 index 00000000..8cc8d477 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/EventDatabaseRepository.java @@ -0,0 +1,80 @@ +package br.com.fullcycle.infrastructure.repositories; + +import br.com.fullcycle.domain.DomainEvent; +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.EventRepository; +import br.com.fullcycle.infrastructure.jpa.entities.EventEntity; +import br.com.fullcycle.infrastructure.jpa.entities.OutboxEntity; +import br.com.fullcycle.infrastructure.jpa.repositories.EventJpaRepository; +import br.com.fullcycle.infrastructure.jpa.repositories.OutboxJpaRepository; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +// Interface Adapter +@Component +public class EventDatabaseRepository implements EventRepository { + + private final EventJpaRepository eventJpaRepository; + private final OutboxJpaRepository outboxJpaRepository; + private final ObjectMapper mapper; + + public EventDatabaseRepository( + final EventJpaRepository EventJpaRepository, + final OutboxJpaRepository outboxJpaRepository, + final ObjectMapper mapper + ) { + this.eventJpaRepository = Objects.requireNonNull(EventJpaRepository); + this.outboxJpaRepository = outboxJpaRepository; + this.mapper = mapper; + } + + @Override + public Optional eventOfId(final EventId anId) { + Objects.requireNonNull(anId, "id cannot be null"); + return this.eventJpaRepository.findById(UUID.fromString(anId.value())) + .map(EventEntity::toEvent); + } + + @Override + @Transactional + public Event create(final Event event) { + return save(event); + } + + @Override + @Transactional + public Event update(Event event) { + return save(event); + } + + @Override + public void deleteAll() { + this.eventJpaRepository.deleteAll(); + } + + private Event save(Event event) { + this.outboxJpaRepository.saveAll( + event.allDomainEvents().stream() + .map(it -> OutboxEntity.of(it, this::toJson)) + .toList() + ); + + return this.eventJpaRepository.save(EventEntity.of(event)) + .toEvent(); + } + + private String toJson(DomainEvent domainEvent) { + try { + return this.mapper.writeValueAsString(domainEvent); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/PartnerDatabaseRepository.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/PartnerDatabaseRepository.java new file mode 100644 index 00000000..46d0335f --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/PartnerDatabaseRepository.java @@ -0,0 +1,66 @@ +package br.com.fullcycle.infrastructure.repositories; + +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.partner.PartnerId; +import br.com.fullcycle.domain.person.Cnpj; +import br.com.fullcycle.domain.person.Email; +import br.com.fullcycle.domain.partner.PartnerRepository; +import br.com.fullcycle.infrastructure.jpa.entities.PartnerEntity; +import br.com.fullcycle.infrastructure.jpa.repositories.PartnerJpaRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +// Interface Adapter +@Component +public class PartnerDatabaseRepository implements PartnerRepository { + + private final PartnerJpaRepository partnerJpaRepository; + + public PartnerDatabaseRepository(final PartnerJpaRepository partnerJpaRepository) { + this.partnerJpaRepository = Objects.requireNonNull(partnerJpaRepository); + } + + @Override + public Optional partnerOfId(final PartnerId anId) { + Objects.requireNonNull(anId, "id cannot be null"); + return this.partnerJpaRepository.findById(UUID.fromString(anId.value())) + .map(PartnerEntity::toPartner); + } + + @Override + public Optional partnerOfCNPJ(final Cnpj cnpj) { + Objects.requireNonNull(cnpj, "Cnpj cannot be null"); + return this.partnerJpaRepository.findByCnpj(cnpj.value()) + .map(PartnerEntity::toPartner); + } + + @Override + public Optional partnerOfEmail(final Email email) { + Objects.requireNonNull(email, "Email cannot be null"); + return this.partnerJpaRepository.findByEmail(email.value()) + .map(PartnerEntity::toPartner); + } + + @Override + @Transactional + public Partner create(final Partner partner) { + return this.partnerJpaRepository.save(PartnerEntity.of(partner)) + .toPartner(); + } + + @Override + @Transactional + public Partner update(Partner partner) { + return this.partnerJpaRepository.save(PartnerEntity.of(partner)) + .toPartner(); + } + + @Override + public void deleteAll() { + this.partnerJpaRepository.deleteAll(); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/TicketDatabaseRepository.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/TicketDatabaseRepository.java new file mode 100644 index 00000000..a714e229 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/repositories/TicketDatabaseRepository.java @@ -0,0 +1,91 @@ +package br.com.fullcycle.infrastructure.repositories; + +import br.com.fullcycle.domain.DomainEvent; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.ticket.Ticket; +import br.com.fullcycle.domain.event.ticket.TicketId; +import br.com.fullcycle.domain.event.ticket.TicketRepository; +import br.com.fullcycle.infrastructure.jpa.entities.OutboxEntity; +import br.com.fullcycle.infrastructure.jpa.entities.TicketEntity; +import br.com.fullcycle.infrastructure.jpa.repositories.OutboxJpaRepository; +import br.com.fullcycle.infrastructure.jpa.repositories.TicketJpaRepository; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.UUID; + +// Interface Adapter +@Component +public class TicketDatabaseRepository implements TicketRepository { + + private final TicketJpaRepository ticketJpaRepository; + private final OutboxJpaRepository outboxJpaRepository; + private final ObjectMapper mapper; + + public TicketDatabaseRepository( + final TicketJpaRepository ticketJpaRepository, + final OutboxJpaRepository outboxJpaRepository, + final ObjectMapper mapper + ) { + this.ticketJpaRepository = Objects.requireNonNull(ticketJpaRepository); + this.outboxJpaRepository = outboxJpaRepository; + this.mapper = mapper; + } + + @Override + public Optional ticketOfId(final TicketId anId) { + Objects.requireNonNull(anId, "id cannot be null"); + return this.ticketJpaRepository.findById(UUID.fromString(anId.value())) + .map(TicketEntity::toTicket); + } + + @Override + @Transactional + public Ticket create(final Ticket ticket) { + return save(ticket); + } + + @Override + @Transactional + public Ticket update(Ticket ticket) { + return save(ticket); + } + + @Override + public void deleteAll() { + this.ticketJpaRepository.deleteAll(); + } + + @Override + public List ticketsByEventId(final EventId anEventId) { + Objects.requireNonNull(anEventId, "id cannot be null"); + return this.ticketJpaRepository.findAllByEventId(UUID.fromString(anEventId.value())).stream() + .map(TicketEntity::toTicket) + .toList(); + } + + private Ticket save(Ticket ticket) { + this.outboxJpaRepository.saveAll( + ticket.allDomainEvents().stream() + .map(it -> OutboxEntity.of(it, this::toJson)) + .toList() + ); + + + return this.ticketJpaRepository.save(TicketEntity.of(ticket)) + .toTicket(); + } + + private String toJson(DomainEvent domainEvent) { + try { + return this.mapper.writeValueAsString(domainEvent); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/CustomerController.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/CustomerController.java new file mode 100644 index 00000000..2e6dac6e --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/CustomerController.java @@ -0,0 +1,59 @@ +package br.com.fullcycle.infrastructure.rest; + +import br.com.fullcycle.application.Presenter; +import br.com.fullcycle.application.customer.CreateCustomerUseCase; +import br.com.fullcycle.application.customer.GetCustomerByIdUseCase; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.infrastructure.dtos.NewCustomerDTO; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.util.Objects; +import java.util.Optional; + +// Adapter +@RestController +@RequestMapping(value = "customers") +public class CustomerController { + + private final CreateCustomerUseCase createCustomerUseCase; + private final GetCustomerByIdUseCase getCustomerByIdUseCase; + private final Presenter, Object> publicGetCustomerPresenter; + private final Presenter, Object> privateGetCustomerPresenter; + + public CustomerController( + final CreateCustomerUseCase createCustomerUseCase, + final GetCustomerByIdUseCase getCustomerByIdUseCase, + final Presenter, Object> privateGetCustomer, + final Presenter, Object> publicGetCustomer + ) { + this.publicGetCustomerPresenter = publicGetCustomer; + this.privateGetCustomerPresenter = privateGetCustomer; + this.createCustomerUseCase = Objects.requireNonNull(createCustomerUseCase); + this.getCustomerByIdUseCase = Objects.requireNonNull(getCustomerByIdUseCase); + } + + @PostMapping + public ResponseEntity create(@RequestBody NewCustomerDTO dto) { + try { + final var output = + createCustomerUseCase.execute(new CreateCustomerUseCase.Input(dto.cpf(), dto.email(), dto.name())); + + return ResponseEntity.created(URI.create("/customers/" + output.id())).body(output); + } catch (ValidationException ex) { + return ResponseEntity.unprocessableEntity().body(ex.getMessage()); + } + } + + @GetMapping("/{id}") + public Object get(@PathVariable String id, @RequestHeader(name = "X-Public", required = false) String xPublic) { + Presenter, Object> presenter = privateGetCustomerPresenter; + + if (xPublic != null) { + presenter = publicGetCustomerPresenter; + } + + return getCustomerByIdUseCase.execute(new GetCustomerByIdUseCase.Input(id), presenter); + } +} \ No newline at end of file diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/EventController.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/EventController.java new file mode 100644 index 00000000..16f09215 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/EventController.java @@ -0,0 +1,93 @@ +package br.com.fullcycle.infrastructure.rest; + +import br.com.fullcycle.application.Presenter; +import br.com.fullcycle.application.event.CancelEventUseCase; +import br.com.fullcycle.application.event.CreateEventUseCase; +import br.com.fullcycle.application.event.GetEventByIdUseCase; +import br.com.fullcycle.application.event.SubscribeCustomerToEventUseCase; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.infrastructure.dtos.NewEventDTO; +import br.com.fullcycle.infrastructure.dtos.SubscribeDTO; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.util.Objects; +import java.util.Optional; + +// Adapter +@RestController +@RequestMapping(value = "events") +public class EventController { + + private final CreateEventUseCase createEventUseCase; + private final SubscribeCustomerToEventUseCase subscribeCustomerToEventUseCase; + private final CancelEventUseCase cancelEventUseCase; + private final GetEventByIdUseCase getEventByIdUseCase; + private final Presenter, Object> privateGetEventPresenter; + private final Presenter, Object> publicGetEventPresenter; + + public EventController( + final CreateEventUseCase createEventUseCase, + final SubscribeCustomerToEventUseCase subscribeCustomerToEventUseCase, + final CancelEventUseCase cancelEventUseCase, + final GetEventByIdUseCase getEventByIdUseCase, + final Presenter, Object> privateGetEvent, + final Presenter, Object> publicGetEvent + ) { + this.createEventUseCase = Objects.requireNonNull(createEventUseCase); + this.subscribeCustomerToEventUseCase = Objects.requireNonNull(subscribeCustomerToEventUseCase); + this.cancelEventUseCase = Objects.requireNonNull(cancelEventUseCase); + this.getEventByIdUseCase = Objects.requireNonNull(getEventByIdUseCase); + this.privateGetEventPresenter = Objects.requireNonNull(privateGetEvent); + this.publicGetEventPresenter = Objects.requireNonNull(publicGetEvent); + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity create(@RequestBody NewEventDTO dto) { + try { + final var output = + createEventUseCase.execute(new CreateEventUseCase.Input(dto.date(), dto.name(), dto.partnerId(), dto.totalSpots())); + + return ResponseEntity.created(URI.create("/events/" + output.id())).body(output); + } catch (ValidationException ex) { + return ResponseEntity.unprocessableEntity().body(ex.getMessage()); + } + } + + @PostMapping(value = "/{id}/subscribe") + public ResponseEntity subscribe(@PathVariable String id, @RequestBody SubscribeDTO dto) { + try { + final var output = + subscribeCustomerToEventUseCase.execute(new SubscribeCustomerToEventUseCase.Input(dto.customerId(), id)); + + return ResponseEntity.ok(output); + } catch (ValidationException ex) { + return ResponseEntity.unprocessableEntity().body(ex.getMessage()); + } + } + + @PostMapping(value = "/{id}/cancel") + public ResponseEntity cancel(@PathVariable String id) { + try { + final var output = cancelEventUseCase.execute(new CancelEventUseCase.Input(id)); + + return ResponseEntity.ok(output); + } catch (ValidationException ex) { + return ResponseEntity.unprocessableEntity().body(ex.getMessage()); + } + } + + @GetMapping("/{id}") + public Object get(@PathVariable String id, @RequestHeader(name = "X-Public", required = false) String xPublic) { + Presenter, Object> presenter = privateGetEventPresenter; + + if (xPublic != null) { + presenter = publicGetEventPresenter; + } + + return getEventByIdUseCase.execute(new GetEventByIdUseCase.Input(id), presenter); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/PartnerController.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/PartnerController.java new file mode 100644 index 00000000..5224b5ee --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/PartnerController.java @@ -0,0 +1,47 @@ +package br.com.fullcycle.infrastructure.rest; + +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.application.partner.CreatePartnerUseCase; +import br.com.fullcycle.application.partner.GetPartnerByIdUseCase; +import br.com.fullcycle.infrastructure.dtos.NewPartnerDTO; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.util.Objects; + +// Adapter +//@RestController +//@RequestMapping(value = "partners") +public class PartnerController { + + private final CreatePartnerUseCase createPartnerUseCase; + private final GetPartnerByIdUseCase getPartnerByIdUseCase; + + public PartnerController( + final CreatePartnerUseCase createPartnerUseCase, + final GetPartnerByIdUseCase getPartnerByIdUseCase + ) { + this.createPartnerUseCase = Objects.requireNonNull(createPartnerUseCase); + this.getPartnerByIdUseCase = Objects.requireNonNull(getPartnerByIdUseCase); + } + + @PostMapping + public ResponseEntity create(@RequestBody NewPartnerDTO dto) { + try { + final var output = + createPartnerUseCase.execute(new CreatePartnerUseCase.Input(dto.cnpj(), dto.email(), dto.name())); + + return ResponseEntity.created(URI.create("/partners/" + output.id())).body(output); + } catch (ValidationException ex) { + return ResponseEntity.unprocessableEntity().body(ex.getMessage()); + } + } + + @GetMapping("/{id}") + public ResponseEntity get(@PathVariable String id) { + return getPartnerByIdUseCase.execute(new GetPartnerByIdUseCase.Input(id)) + .map(ResponseEntity::ok) + .orElseGet(ResponseEntity.notFound()::build); + } +} \ No newline at end of file diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/PartnerFnController.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/PartnerFnController.java new file mode 100644 index 00000000..6d63d81d --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/PartnerFnController.java @@ -0,0 +1,53 @@ +package br.com.fullcycle.infrastructure.rest; + +import br.com.fullcycle.application.partner.CreatePartnerUseCase; +import br.com.fullcycle.application.partner.GetPartnerByIdUseCase; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.infrastructure.dtos.NewPartnerDTO; +import br.com.fullcycle.infrastructure.http.HttpRouter; +import br.com.fullcycle.infrastructure.http.HttpRouter.HttpRequest; +import br.com.fullcycle.infrastructure.http.HttpRouter.HttpResponse; + +import java.net.URI; +import java.util.Objects; + +// Adapter +public class PartnerFnController { + + private final CreatePartnerUseCase createPartnerUseCase; + private final GetPartnerByIdUseCase getPartnerByIdUseCase; + + public PartnerFnController( + final CreatePartnerUseCase createPartnerUseCase, + final GetPartnerByIdUseCase getPartnerByIdUseCase + ) { + this.createPartnerUseCase = Objects.requireNonNull(createPartnerUseCase); + this.getPartnerByIdUseCase = Objects.requireNonNull(getPartnerByIdUseCase); + } + + public HttpRouter bind(final HttpRouter router) { + router.GET("/partners/{id}", this::get); + router.POST("/partners", this::create); + return router; + } + + private HttpResponse create(final HttpRequest req) { + try { + final var dto = req.body(NewPartnerDTO.class); + + final var output = + createPartnerUseCase.execute(new CreatePartnerUseCase.Input(dto.cnpj(), dto.email(), dto.name())); + + return HttpResponse.created(URI.create("/partners/" + output.id())).body(output); + } catch (ValidationException ex) { + return HttpResponse.unprocessableEntity().body(ex.getMessage()); + } + } + + private HttpResponse get(final HttpRequest req) { + final String id = req.pathParam("id"); + return getPartnerByIdUseCase.execute(new GetPartnerByIdUseCase.Input(id)) + .map(HttpResponse::ok) + .orElseGet(HttpResponse.notFound()::build); + } +} \ No newline at end of file diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/GetCustomerByIdResponseEntity.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/GetCustomerByIdResponseEntity.java new file mode 100644 index 00000000..4a14ee7f --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/GetCustomerByIdResponseEntity.java @@ -0,0 +1,28 @@ +package br.com.fullcycle.infrastructure.rest.presenters; + +import br.com.fullcycle.application.Presenter; +import br.com.fullcycle.application.customer.GetCustomerByIdUseCase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component("privateGetCustomer") +public class GetCustomerByIdResponseEntity implements Presenter, Object> { + + private static final Logger LOG = LoggerFactory.getLogger(GetCustomerByIdResponseEntity.class); + + @Override + public ResponseEntity present(final Optional output) { + return output.map(ResponseEntity::ok) + .orElseGet(ResponseEntity.notFound()::build); + } + + @Override + public ResponseEntity present(Throwable error) { + LOG.error("An error was observer at GetCustomerByIdUseCase", error); + return ResponseEntity.notFound().build(); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/GetEventByIdResponseEntity.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/GetEventByIdResponseEntity.java new file mode 100644 index 00000000..17477203 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/GetEventByIdResponseEntity.java @@ -0,0 +1,28 @@ +package br.com.fullcycle.infrastructure.rest.presenters; + +import br.com.fullcycle.application.Presenter; +import br.com.fullcycle.application.event.GetEventByIdUseCase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component("privateGetEvent") +public class GetEventByIdResponseEntity implements Presenter, Object> { + + private static final Logger LOG = LoggerFactory.getLogger(GetEventByIdResponseEntity.class); + + @Override + public ResponseEntity present(final Optional output) { + return output.map(ResponseEntity::ok) + .orElseGet(ResponseEntity.notFound()::build); + } + + @Override + public ResponseEntity present(Throwable error) { + LOG.error("An error was observer at GetEventByIdUseCase", error); + return ResponseEntity.notFound().build(); + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/PublicGetCustomerByIdString.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/PublicGetCustomerByIdString.java new file mode 100644 index 00000000..5429b7b5 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/PublicGetCustomerByIdString.java @@ -0,0 +1,27 @@ +package br.com.fullcycle.infrastructure.rest.presenters; + +import br.com.fullcycle.application.Presenter; +import br.com.fullcycle.application.customer.GetCustomerByIdUseCase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component("publicGetCustomer") +public class PublicGetCustomerByIdString implements Presenter, Object> { + + private static final Logger LOG = LoggerFactory.getLogger(PublicGetCustomerByIdString.class); + + @Override + public String present(final Optional output) { + return output.map(o -> o.id()) + .orElseGet(() -> "not found"); + } + + @Override + public String present(Throwable error) { + LOG.error("An error was observer at GetCustomerByIdUseCase", error); + return "not found"; + } +} diff --git a/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/PublicGetEventByIdResponseEntity.java b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/PublicGetEventByIdResponseEntity.java new file mode 100644 index 00000000..0cd5e514 --- /dev/null +++ b/infrastructure/src/main/java/br/com/fullcycle/infrastructure/rest/presenters/PublicGetEventByIdResponseEntity.java @@ -0,0 +1,31 @@ +package br.com.fullcycle.infrastructure.rest.presenters; + +import br.com.fullcycle.application.Presenter; +import br.com.fullcycle.application.event.GetEventByIdUseCase; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; + +import java.util.Optional; + +@Component("publicGetEvent") +public class PublicGetEventByIdResponseEntity implements Presenter, Object> { + + private static final Logger LOG = LoggerFactory.getLogger(PublicGetEventByIdResponseEntity.class); + + @Override + public ResponseEntity present(final Optional output) { + return output.map(o -> ResponseEntity.ok((Object) new PublicEvent(o.id(), o.status()))) + .orElseGet(ResponseEntity.notFound()::build); + } + + @Override + public ResponseEntity present(Throwable error) { + LOG.error("An error was observer at GetEventByIdUseCase", error); + return ResponseEntity.notFound().build(); + } + + public record PublicEvent(String id, String status) { + } +} diff --git a/src/main/resources/application-test.properties b/infrastructure/src/main/resources/application-test.properties similarity index 100% rename from src/main/resources/application-test.properties rename to infrastructure/src/main/resources/application-test.properties diff --git a/src/main/resources/application.properties b/infrastructure/src/main/resources/application.properties similarity index 75% rename from src/main/resources/application.properties rename to infrastructure/src/main/resources/application.properties index 285d5eb3..16ac00b9 100644 --- a/src/main/resources/application.properties +++ b/infrastructure/src/main/resources/application.properties @@ -3,4 +3,6 @@ spring.datasource.username=root spring.datasource.password=root spring.jpa.hibernate.ddl-auto=update spring.jpa.open-in-view=false -spring.jpa.show-sql=true \ No newline at end of file +spring.jpa.show-sql=true + +spring.graphql.graphiql.enabled=true \ No newline at end of file diff --git a/infrastructure/src/main/resources/graphql/schema.gqls b/infrastructure/src/main/resources/graphql/schema.gqls new file mode 100644 index 00000000..268310d6 --- /dev/null +++ b/infrastructure/src/main/resources/graphql/schema.gqls @@ -0,0 +1,70 @@ +type Query { + customerOfId(id: ID!): Customer + partnerOfId(id: ID!): Partner + eventOfId(id: ID!): Event +} + +type Mutation { + createCustomer(input: CustomerInput): Customer! + createEvent(input: EventInput): Event! + createPartner(input: PartnerInput): Partner! + subscribeCustomerToEvent(input: SubscribeInput): Subscribe! + cancelEvent(id: ID!): CancelEventResult! +} + +type Customer { + id: ID! + name: String + email: String + cpf: String +} + +input CustomerInput { + name: String + email: String + cpf: String +} + +type Event { + id: ID! + date: String! + totalSpots: Int! + name: String! + status: String +} + +input EventInput { + date: String + totalSpots: Int + name: String + partnerId: ID +} + +type CancelEventResult { + id: ID! + status: String! +} + +type Partner { + id: ID! + name: String + email: String + cnpj: String +} + +input PartnerInput { + name: String + email: String + cnpj: String +} + +type Subscribe { + eventId: ID! + ticketStatus: String! + reservationDate: String! +} + +input SubscribeInput { + customerId: ID + eventId: ID +} \ No newline at end of file diff --git a/infrastructure/src/test/java/br/com/fullcycle/IntegrationTest.java b/infrastructure/src/test/java/br/com/fullcycle/IntegrationTest.java new file mode 100644 index 00000000..0e5f9aa2 --- /dev/null +++ b/infrastructure/src/test/java/br/com/fullcycle/IntegrationTest.java @@ -0,0 +1,10 @@ +package br.com.fullcycle; + +import br.com.fullcycle.infrastructure.Main; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +@SpringBootTest(classes = Main.class) +public abstract class IntegrationTest { +} diff --git a/infrastructure/src/test/java/br/com/fullcycle/application/CreateCustomerUseCaseIT.java b/infrastructure/src/test/java/br/com/fullcycle/application/CreateCustomerUseCaseIT.java new file mode 100644 index 00000000..991df4f6 --- /dev/null +++ b/infrastructure/src/test/java/br/com/fullcycle/application/CreateCustomerUseCaseIT.java @@ -0,0 +1,90 @@ +package br.com.fullcycle.application; + +import br.com.fullcycle.IntegrationTest; +import br.com.fullcycle.application.customer.CreateCustomerUseCase; +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.customer.CustomerRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +public class CreateCustomerUseCaseIT extends IntegrationTest { + + @Autowired + private CreateCustomerUseCase useCase; + + @Autowired + private CustomerRepository customerRepository; + + @BeforeEach + void setUp() { + customerRepository.deleteAll(); + } + + @Test + @DisplayName("Deve criar um cliente") + public void testCreateCustomer() { + // given + final var expectedCPF = "123.456.789-01"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + + final var createInput = new CreateCustomerUseCase.Input(expectedCPF, expectedEmail, expectedName); + + // when + final var output = useCase.execute(createInput); + + // then + Assertions.assertNotNull(output.id()); + Assertions.assertEquals(expectedCPF, output.cpf()); + Assertions.assertEquals(expectedEmail, output.email()); + Assertions.assertEquals(expectedName, output.name()); + } + + @Test + @DisplayName("Não deve cadastrar um cliente com CPF duplicado") + public void testCreateWithDuplicatedCPFShouldFail() throws Exception { + // given + final var expectedCPF = "123.456.789-01"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + final var expectedError = "Customer already exists"; + + createCustomer(expectedCPF, expectedEmail, expectedName); + + final var createInput = new CreateCustomerUseCase.Input(expectedCPF, expectedEmail, expectedName); + + // when + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(createInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } + + @Test + @DisplayName("Não deve cadastrar um cliente com e-mail duplicado") + public void testCreateWithDuplicatedEmailShouldFail() throws Exception { + // given + final var expectedCPF = "123.456.789-01"; + final var expectedEmail = "john.doe@gmail.com"; + final var expectedName = "John Doe"; + final var expectedError = "Customer already exists"; + + createCustomer("231.321.312-31", expectedEmail, expectedName); + + final var createInput = new CreateCustomerUseCase.Input(expectedCPF, expectedEmail, expectedName); + + // when + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(createInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } + + private Customer createCustomer(final String cpf, final String email, final String name) { + return customerRepository.create(Customer.newCustomer(name, cpf, email)); + } +} diff --git a/infrastructure/src/test/java/br/com/fullcycle/application/usecases/CancelEventUseCaseIT.java b/infrastructure/src/test/java/br/com/fullcycle/application/usecases/CancelEventUseCaseIT.java new file mode 100644 index 00000000..8e7edab4 --- /dev/null +++ b/infrastructure/src/test/java/br/com/fullcycle/application/usecases/CancelEventUseCaseIT.java @@ -0,0 +1,67 @@ +package br.com.fullcycle.application.usecases; + +import br.com.fullcycle.IntegrationTest; +import br.com.fullcycle.application.event.CancelEventUseCase; +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.EventRepository; +import br.com.fullcycle.domain.event.EventStatus; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.partner.PartnerRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class CancelEventUseCaseIT extends IntegrationTest { + + @Autowired + private CancelEventUseCase useCase; + + @Autowired + private EventRepository eventRepository; + + @Autowired + private PartnerRepository partnerRepository; + + @BeforeEach + void setUp() { + eventRepository.deleteAll(); + partnerRepository.deleteAll(); + } + + @Test + @DisplayName("Deve cancelar um evento persistido") + public void testCancelEvent() throws Exception { + // given + final var aPartner = partnerRepository.create(Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com")); + final var anEvent = eventRepository.create(Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner)); + + final var cancelInput = new CancelEventUseCase.Input(anEvent.eventId().value()); + + // when + final var output = useCase.execute(cancelInput); + + // then + Assertions.assertEquals("CANCELLED", output.status()); + + final var actualEvent = eventRepository.eventOfId(EventId.with(anEvent.eventId().value())).get(); + Assertions.assertEquals(EventStatus.CANCELLED, actualEvent.status()); + } + + @Test + @DisplayName("Não deve cancelar um evento que não existe") + public void testCancelEventThatDoesNotExist() throws Exception { + // given + final var expectedError = "Event not found"; + final var cancelInput = new CancelEventUseCase.Input(EventId.unique().value()); + + // when + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(cancelInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } +} diff --git a/infrastructure/src/test/java/br/com/fullcycle/application/usecases/CreateEventUseCaseIT.java b/infrastructure/src/test/java/br/com/fullcycle/application/usecases/CreateEventUseCaseIT.java new file mode 100644 index 00000000..5c92f960 --- /dev/null +++ b/infrastructure/src/test/java/br/com/fullcycle/application/usecases/CreateEventUseCaseIT.java @@ -0,0 +1,80 @@ +package br.com.fullcycle.application.usecases; + +import br.com.fullcycle.IntegrationTest; +import br.com.fullcycle.application.event.CreateEventUseCase; +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.partner.PartnerId; +import br.com.fullcycle.domain.exceptions.ValidationException; +import br.com.fullcycle.domain.event.EventRepository; +import br.com.fullcycle.domain.partner.PartnerRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class CreateEventUseCaseIT extends IntegrationTest { + + @Autowired + private CreateEventUseCase useCase; + + @Autowired + private EventRepository eventRepository; + + @Autowired + private PartnerRepository partnerRepository; + + @BeforeEach + void setUp() { + eventRepository.deleteAll(); + partnerRepository.deleteAll(); + } + + @Test + @DisplayName("Deve criar um evento") + public void testCreate() throws Exception { + // given + final var partner = createPartner("41.536.538/0001-00", "john.doe@gmail.com", "John Doe"); + final var expectedDate = "2021-01-01"; + final var expectedName = "Disney on Ice"; + final var expectedTotalSpots = 10; + final var expectedPartnerId = partner.partnerId().value(); + + final var createInput = + new CreateEventUseCase.Input(expectedDate, expectedName, expectedPartnerId, expectedTotalSpots); + + // when + final var output = useCase.execute(createInput); + + // then + Assertions.assertNotNull(output.id()); + Assertions.assertEquals(expectedDate, output.date()); + Assertions.assertEquals(expectedName, output.name()); + Assertions.assertEquals(expectedTotalSpots, output.totalSpots()); + Assertions.assertEquals(expectedPartnerId, output.partnerId()); + } + + @Test + @DisplayName("Não deve criar um evento quando o Partner não for encontrado") + public void testCreateEvent_whenPartnerDoesntExists_ShouldThrowError() throws Exception { + // given + final var expectedDate = "2021-01-01"; + final var expectedName = "Disney on Ice"; + final var expectedTotalSpots = 10; + final var expectedPartnerId = PartnerId.unique().value(); + final var expectedError = "Partner not found"; + + final var createInput = + new CreateEventUseCase.Input(expectedDate, expectedName, expectedPartnerId, expectedTotalSpots); + + // when + final var actualException = Assertions.assertThrows(ValidationException.class, () -> useCase.execute(createInput)); + + // then + Assertions.assertEquals(expectedError, actualException.getMessage()); + } + + private Partner createPartner(final String cnpj, final String email, final String name) { + return partnerRepository.create(Partner.newPartner(name, cnpj, email)); + } +} \ No newline at end of file diff --git a/src/test/java/br/com/fullcycle/hexagonal/MainTests.java b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/MainTests.java similarity index 85% rename from src/test/java/br/com/fullcycle/hexagonal/MainTests.java rename to infrastructure/src/test/java/br/com/fullcycle/infrastructure/MainTests.java index b6551348..dba72f63 100644 --- a/src/test/java/br/com/fullcycle/hexagonal/MainTests.java +++ b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/MainTests.java @@ -1,4 +1,4 @@ -package br.com.fullcycle.hexagonal; +package br.com.fullcycle.infrastructure; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; diff --git a/infrastructure/src/test/java/br/com/fullcycle/infrastructure/gateways/ConsumerQueueGatewayCancelEventIT.java b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/gateways/ConsumerQueueGatewayCancelEventIT.java new file mode 100644 index 00000000..cb83d4fb --- /dev/null +++ b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/gateways/ConsumerQueueGatewayCancelEventIT.java @@ -0,0 +1,84 @@ +package br.com.fullcycle.infrastructure.gateways; + +import br.com.fullcycle.IntegrationTest; +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.customer.CustomerRepository; +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.event.EventCancelled; +import br.com.fullcycle.domain.event.EventRepository; +import br.com.fullcycle.domain.event.ticket.Ticket; +import br.com.fullcycle.domain.event.ticket.TicketRepository; +import br.com.fullcycle.domain.event.ticket.TicketStatus; +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.partner.PartnerRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +class ConsumerQueueGatewayCancelEventIT extends IntegrationTest { + + @Autowired + private ConsumerQueueGateway consumerQueueGateway; + + @Autowired + private ObjectMapper mapper; + + @Autowired + private EventRepository eventRepository; + + @Autowired + private TicketRepository ticketRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private PartnerRepository partnerRepository; + + @BeforeEach + void setUp() { + ticketRepository.deleteAll(); + eventRepository.deleteAll(); + customerRepository.deleteAll(); + partnerRepository.deleteAll(); + } + + @Test + @DisplayName("Deve cancelar todos os tickets do evento ao processar um EventCancelled recebido pela fila") + public void testCascadeCancelsTicketsWhenEventCancelledIsConsumedFromQueue() throws Exception { + // given + final var aPartner = partnerRepository.create(Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com")); + final var anEvent = eventRepository.create(Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner)); + + final var aCustomer = customerRepository.create(Customer.newCustomer("Gabriel Doe", "123.456.789-01", "gabriel.doe@gmail.com")); + final var aCustomer2 = customerRepository.create(Customer.newCustomer("Pedro Doe", "123.111.789-01", "pedro.doe@gmail.com")); + + ticketRepository.create(Ticket.newTicket(aCustomer.customerId(), anEvent.eventId())); + ticketRepository.create(Ticket.newTicket(aCustomer2.customerId(), anEvent.eventId())); + + final var eventCancelled = new EventCancelled(anEvent.eventId()); + final var json = mapper.writeValueAsString(eventCancelled); + + // when: dispara o EventCancelled pelo caminho real do gateway, o mesmo usado pelo OutboxRelay + consumerQueueGateway.publish(json); + + // then: publish é assíncrono, então aguarda o processamento com timeout + final var deadline = Instant.now().plus(Duration.ofSeconds(5)); + List ticketsOfEvent; + do { + Thread.sleep(100); + ticketsOfEvent = ticketRepository.ticketsByEventId(anEvent.eventId()); + } while (Instant.now().isBefore(deadline) + && ticketsOfEvent.stream().anyMatch(it -> it.status() != TicketStatus.CANCELLED)); + + Assertions.assertEquals(2, ticketsOfEvent.size()); + Assertions.assertTrue(ticketsOfEvent.stream().allMatch(it -> it.status() == TicketStatus.CANCELLED)); + } +} diff --git a/infrastructure/src/test/java/br/com/fullcycle/infrastructure/repositories/TicketDatabaseRepositoryIT.java b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/repositories/TicketDatabaseRepositoryIT.java new file mode 100644 index 00000000..80095dff --- /dev/null +++ b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/repositories/TicketDatabaseRepositoryIT.java @@ -0,0 +1,73 @@ +package br.com.fullcycle.infrastructure.repositories; + +import br.com.fullcycle.IntegrationTest; +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.customer.CustomerRepository; +import br.com.fullcycle.domain.event.Event; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.event.EventRepository; +import br.com.fullcycle.domain.event.ticket.Ticket; +import br.com.fullcycle.domain.event.ticket.TicketRepository; +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.partner.PartnerRepository; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +class TicketDatabaseRepositoryIT extends IntegrationTest { + + @Autowired + private TicketRepository ticketRepository; + + @Autowired + private EventRepository eventRepository; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private PartnerRepository partnerRepository; + + @BeforeEach + void setUp() { + ticketRepository.deleteAll(); + eventRepository.deleteAll(); + customerRepository.deleteAll(); + partnerRepository.deleteAll(); + } + + @Test + @DisplayName("Deve buscar os tickets de um evento") + public void testTicketsByEventId() throws Exception { + // given + final var aPartner = partnerRepository.create(Partner.newPartner("John Doe", "41.536.538/0001-00", "john.doe@gmail.com")); + final var anEvent = eventRepository.create(Event.newEvent("Disney on Ice", "2021-01-01", 10, aPartner)); + final var anotherEvent = eventRepository.create(Event.newEvent("Rock in Rio", "2021-02-01", 10, aPartner)); + + final var aCustomer = customerRepository.create(Customer.newCustomer("Gabriel Doe", "123.456.789-01", "gabriel.doe@gmail.com")); + final var aCustomer2 = customerRepository.create(Customer.newCustomer("Pedro Doe", "123.111.789-01", "pedro.doe@gmail.com")); + + ticketRepository.create(Ticket.newTicket(aCustomer.customerId(), anEvent.eventId())); + ticketRepository.create(Ticket.newTicket(aCustomer2.customerId(), anEvent.eventId())); + ticketRepository.create(Ticket.newTicket(aCustomer.customerId(), anotherEvent.eventId())); + + // when + final var actualTickets = ticketRepository.ticketsByEventId(anEvent.eventId()); + + // then + Assertions.assertEquals(2, actualTickets.size()); + Assertions.assertTrue(actualTickets.stream().allMatch(it -> it.eventId().equals(anEvent.eventId()))); + } + + @Test + @DisplayName("Deve retornar lista vazia para um evento sem tickets") + public void testTicketsByEventIdWithoutTickets() throws Exception { + // when + final var actualTickets = ticketRepository.ticketsByEventId(EventId.unique()); + + // then + Assertions.assertTrue(actualTickets.isEmpty()); + } +} diff --git a/src/test/java/br/com/fullcycle/hexagonal/controllers/CustomerControllerTest.java b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/rest/CustomerControllerTest.java similarity index 60% rename from src/test/java/br/com/fullcycle/hexagonal/controllers/CustomerControllerTest.java rename to infrastructure/src/test/java/br/com/fullcycle/infrastructure/rest/CustomerControllerTest.java index ded41912..020d915d 100644 --- a/src/test/java/br/com/fullcycle/hexagonal/controllers/CustomerControllerTest.java +++ b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/rest/CustomerControllerTest.java @@ -1,9 +1,14 @@ -package br.com.fullcycle.hexagonal.controllers; +package br.com.fullcycle.infrastructure.rest; -import br.com.fullcycle.hexagonal.dtos.CustomerDTO; -import br.com.fullcycle.hexagonal.repositories.CustomerRepository; +import br.com.fullcycle.application.customer.CreateCustomerUseCase; +import br.com.fullcycle.application.customer.GetCustomerByIdUseCase; +import br.com.fullcycle.infrastructure.dtos.NewCustomerDTO; +import br.com.fullcycle.infrastructure.jpa.repositories.CustomerJpaRepository; import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -25,10 +30,10 @@ public class CustomerControllerTest { private ObjectMapper mapper; @Autowired - private CustomerRepository customerRepository; + private CustomerJpaRepository customerRepository; - @AfterEach - void tearDown() { + @BeforeEach + void setUp() { customerRepository.deleteAll(); } @@ -36,10 +41,7 @@ void tearDown() { @DisplayName("Deve criar um cliente") public void testCreate() throws Exception { - var customer = new CustomerDTO(); - customer.setCpf("12345678901"); - customer.setEmail("john.doe@gmail.com"); - customer.setName("John Doe"); + var customer = new NewCustomerDTO("123.456.789-01", "john.doe@gmail.com", "John Doe"); final var result = this.mvc.perform( MockMvcRequestBuilders.post("/customers") @@ -48,23 +50,20 @@ public void testCreate() throws Exception { ) .andExpect(MockMvcResultMatchers.status().isCreated()) .andExpect(MockMvcResultMatchers.header().exists("Location")) - .andExpect(MockMvcResultMatchers.jsonPath("$.id").isNumber()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").isString()) .andReturn().getResponse().getContentAsByteArray(); - var actualResponse = mapper.readValue(result, CustomerDTO.class); - Assertions.assertEquals(customer.getName(), actualResponse.getName()); - Assertions.assertEquals(customer.getCpf(), actualResponse.getCpf()); - Assertions.assertEquals(customer.getEmail(), actualResponse.getEmail()); + var actualResponse = mapper.readValue(result, NewCustomerDTO.class); + Assertions.assertEquals(customer.name(), actualResponse.name()); + Assertions.assertEquals(customer.cpf(), actualResponse.cpf()); + Assertions.assertEquals(customer.email(), actualResponse.email()); } @Test @DisplayName("Não deve cadastrar um cliente com CPF duplicado") public void testCreateWithDuplicatedCPFShouldFail() throws Exception { - var customer = new CustomerDTO(); - customer.setCpf("12345678901"); - customer.setEmail("john.doe@gmail.com"); - customer.setName("John Doe"); + var customer = new NewCustomerDTO("123.456.789-01", "john.doe@gmail.com", "John Doe"); // Cria o primeiro cliente this.mvc.perform( @@ -74,10 +73,10 @@ public void testCreateWithDuplicatedCPFShouldFail() throws Exception { ) .andExpect(MockMvcResultMatchers.status().isCreated()) .andExpect(MockMvcResultMatchers.header().exists("Location")) - .andExpect(MockMvcResultMatchers.jsonPath("$.id").isNumber()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").isString()) .andReturn().getResponse().getContentAsByteArray(); - customer.setEmail("john2@gmail.com"); + customer = new NewCustomerDTO("123.456.789-01", "john2@gmail.com", "John Doe"); // Tenta criar o segundo cliente com o mesmo CPF this.mvc.perform( @@ -93,10 +92,7 @@ public void testCreateWithDuplicatedCPFShouldFail() throws Exception { @DisplayName("Não deve cadastrar um cliente com e-mail duplicado") public void testCreateWithDuplicatedEmailShouldFail() throws Exception { - var customer = new CustomerDTO(); - customer.setCpf("12345618901"); - customer.setEmail("john.doe@gmail.com"); - customer.setName("John Doe"); + var customer = new NewCustomerDTO("123.456.789-01", "john.doe@gmail.com", "John Doe"); // Cria o primeiro cliente this.mvc.perform( @@ -106,10 +102,10 @@ public void testCreateWithDuplicatedEmailShouldFail() throws Exception { ) .andExpect(MockMvcResultMatchers.status().isCreated()) .andExpect(MockMvcResultMatchers.header().exists("Location")) - .andExpect(MockMvcResultMatchers.jsonPath("$.id").isNumber()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").isString()) .andReturn().getResponse().getContentAsByteArray(); - customer.setCpf("99999918901"); + customer = new NewCustomerDTO("999.999.189-01", "john.doe@gmail.com", "John Doe"); // Tenta criar o segundo cliente com o mesmo CPF this.mvc.perform( @@ -125,10 +121,7 @@ public void testCreateWithDuplicatedEmailShouldFail() throws Exception { @DisplayName("Deve obter um cliente por id") public void testGet() throws Exception { - var customer = new CustomerDTO(); - customer.setCpf("12345678901"); - customer.setEmail("john.doe@gmail.com"); - customer.setName("John Doe"); + var customer = new NewCustomerDTO("123.456.789-01", "john.doe@gmail.com", "John Doe"); final var createResult = this.mvc.perform( MockMvcRequestBuilders.post("/customers") @@ -137,7 +130,7 @@ public void testGet() throws Exception { ) .andReturn().getResponse().getContentAsByteArray(); - var customerId = mapper.readValue(createResult, CustomerDTO.class).getId(); + var customerId = mapper.readValue(createResult, CreateCustomerUseCase.Output.class).id(); final var result = this.mvc.perform( MockMvcRequestBuilders.get("/customers/{id}", customerId) @@ -145,10 +138,35 @@ public void testGet() throws Exception { .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn().getResponse().getContentAsByteArray(); - var actualResponse = mapper.readValue(result, CustomerDTO.class); - Assertions.assertEquals(customerId, actualResponse.getId()); - Assertions.assertEquals(customer.getName(), actualResponse.getName()); - Assertions.assertEquals(customer.getCpf(), actualResponse.getCpf()); - Assertions.assertEquals(customer.getEmail(), actualResponse.getEmail()); + var actualResponse = mapper.readValue(result, GetCustomerByIdUseCase.Output.class); + Assertions.assertEquals(customerId, actualResponse.id()); + Assertions.assertEquals(customer.name(), actualResponse.name()); + Assertions.assertEquals(customer.cpf(), actualResponse.cpf()); + Assertions.assertEquals(customer.email(), actualResponse.email()); + } + + @Test + @DisplayName("Deve obter um cliente por id com X-Public") + public void testGetPublic() throws Exception { + + var customer = new NewCustomerDTO("123.456.789-01", "john.doe@gmail.com", "John Doe"); + + final var createResult = this.mvc.perform( + MockMvcRequestBuilders.post("/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(customer)) + ) + .andReturn().getResponse().getContentAsByteArray(); + + var customerId = mapper.readValue(createResult, CreateCustomerUseCase.Output.class).id(); + + final var actualResponse = this.mvc.perform( + MockMvcRequestBuilders.get("/customers/{id}", customerId) + .header("X-Public", "true") + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn().getResponse().getContentAsByteArray(); + + Assertions.assertEquals(customerId, new String(actualResponse)); } } diff --git a/infrastructure/src/test/java/br/com/fullcycle/infrastructure/rest/EventControllerTest.java b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/rest/EventControllerTest.java new file mode 100644 index 00000000..b111653f --- /dev/null +++ b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/rest/EventControllerTest.java @@ -0,0 +1,206 @@ +package br.com.fullcycle.infrastructure.rest; + +import br.com.fullcycle.domain.customer.Customer; +import br.com.fullcycle.domain.event.EventId; +import br.com.fullcycle.domain.partner.Partner; +import br.com.fullcycle.domain.customer.CustomerRepository; +import br.com.fullcycle.domain.event.EventRepository; +import br.com.fullcycle.domain.partner.PartnerRepository; +import br.com.fullcycle.application.event.CreateEventUseCase; +import br.com.fullcycle.infrastructure.dtos.NewEventDTO; +import br.com.fullcycle.infrastructure.dtos.SubscribeDTO; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; +import org.springframework.transaction.annotation.Transactional; + +@ActiveProfiles("test") +@AutoConfigureMockMvc +@SpringBootTest +class EventControllerTest { + + @Autowired + private MockMvc mvc; + + @Autowired + private ObjectMapper mapper; + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private PartnerRepository partnerRepository; + + @Autowired + private EventRepository eventRepository; + + private Customer johnDoe; + private Partner disney; + + @BeforeEach + void setUp() { + eventRepository.deleteAll(); + customerRepository.deleteAll(); + partnerRepository.deleteAll(); + + johnDoe = customerRepository.create(Customer.newCustomer("John Doe", "123.456.789-00", "john@gmail.com")); + disney = partnerRepository.create(Partner.newPartner("Disney", "45.123.123/0001-12", "disney@gmail.com")); + } + + @Test + @DisplayName("Deve criar um evento") + public void testCreate() throws Exception { + + var event = new NewEventDTO("Disney on Ice", "2021-01-01", 100, disney.partnerId().value()); + + final var result = this.mvc.perform( + MockMvcRequestBuilders.post("/events") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(event)) + ) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").isString()) + .andReturn().getResponse().getContentAsByteArray(); + + var actualResponse = mapper.readValue(result, NewEventDTO.class); + Assertions.assertEquals(event.date(), actualResponse.date()); + Assertions.assertEquals(event.totalSpots(), actualResponse.totalSpots()); + Assertions.assertEquals(event.name(), actualResponse.name()); + } + + @Test + @Transactional + @DisplayName("Deve comprar um ticket de um evento") + public void testReserveTicket() throws Exception { + + var event = new NewEventDTO("Disney on Ice", "2021-01-01", 100, disney.partnerId().value()); + + final var createResult = this.mvc.perform( + MockMvcRequestBuilders.post("/events") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(event)) + ) + .andExpect(MockMvcResultMatchers.status().isCreated()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").isString()) + .andReturn().getResponse().getContentAsByteArray(); + + var eventId = mapper.readValue(createResult, CreateEventUseCase.Output.class).id(); + + var sub = new SubscribeDTO(johnDoe.customerId().value(), null); + + this.mvc.perform( + MockMvcRequestBuilders.post("/events/{id}/subscribe", eventId) + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(sub)) + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andReturn().getResponse().getContentAsByteArray(); + + var actualEvent = eventRepository.eventOfId(EventId.with(eventId)).get(); + Assertions.assertEquals(1, actualEvent.allTickets().size()); + } + + @Test + @DisplayName("Deve cancelar um evento") + public void testCancel() throws Exception { + var event = new NewEventDTO("Disney on Ice", "2021-01-01", 100, disney.partnerId().value()); + + final var createResult = this.mvc.perform( + MockMvcRequestBuilders.post("/events") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(event)) + ) + .andReturn().getResponse().getContentAsByteArray(); + + var eventId = mapper.readValue(createResult, CreateEventUseCase.Output.class).id(); + + this.mvc.perform(MockMvcRequestBuilders.post("/events/{id}/cancel", eventId)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(eventId)) + .andExpect(MockMvcResultMatchers.jsonPath("$.status").value("CANCELLED")); + } + + @Test + @DisplayName("Não deve cancelar um evento já cancelado") + public void testCancelAlreadyCancelledShouldFail() throws Exception { + var event = new NewEventDTO("Disney on Ice", "2021-01-01", 100, disney.partnerId().value()); + + final var createResult = this.mvc.perform( + MockMvcRequestBuilders.post("/events") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(event)) + ) + .andReturn().getResponse().getContentAsByteArray(); + + var eventId = mapper.readValue(createResult, CreateEventUseCase.Output.class).id(); + + this.mvc.perform(MockMvcRequestBuilders.post("/events/{id}/cancel", eventId)) + .andExpect(MockMvcResultMatchers.status().isOk()); + + this.mvc.perform(MockMvcRequestBuilders.post("/events/{id}/cancel", eventId)) + .andExpect(MockMvcResultMatchers.status().isUnprocessableEntity()) + .andExpect(MockMvcResultMatchers.content().string("Event already cancelled")); + } + + @Test + @DisplayName("Deve obter um evento por id") + public void testGet() throws Exception { + var event = new NewEventDTO("Disney on Ice", "2021-01-01", 100, disney.partnerId().value()); + + final var createResult = this.mvc.perform( + MockMvcRequestBuilders.post("/events") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(event)) + ) + .andReturn().getResponse().getContentAsByteArray(); + + var eventId = mapper.readValue(createResult, CreateEventUseCase.Output.class).id(); + + this.mvc.perform(MockMvcRequestBuilders.get("/events/{id}", eventId)) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(eventId)) + .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("Disney on Ice")) + .andExpect(MockMvcResultMatchers.jsonPath("$.status").value("ACTIVE")); + } + + @Test + @DisplayName("Deve obter um evento por id com X-Public") + public void testGetPublic() throws Exception { + var event = new NewEventDTO("Disney on Ice", "2021-01-01", 100, disney.partnerId().value()); + + final var createResult = this.mvc.perform( + MockMvcRequestBuilders.post("/events") + .contentType(MediaType.APPLICATION_JSON) + .content(mapper.writeValueAsString(event)) + ) + .andReturn().getResponse().getContentAsByteArray(); + + var eventId = mapper.readValue(createResult, CreateEventUseCase.Output.class).id(); + + this.mvc.perform( + MockMvcRequestBuilders.get("/events/{id}", eventId) + .header("X-Public", "true") + ) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(eventId)) + .andExpect(MockMvcResultMatchers.jsonPath("$.status").value("ACTIVE")) + .andExpect(MockMvcResultMatchers.jsonPath("$.name").doesNotExist()); + } + + @Test + @DisplayName("Deve retornar 404 ao consultar um evento que não existe") + public void testGetNotFound() throws Exception { + this.mvc.perform(MockMvcRequestBuilders.get("/events/{id}", EventId.unique().value())) + .andExpect(MockMvcResultMatchers.status().isNotFound()); + } +} \ No newline at end of file diff --git a/src/test/java/br/com/fullcycle/hexagonal/controllers/PartnerControllerTest.java b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/rest/PartnerControllerTest.java similarity index 73% rename from src/test/java/br/com/fullcycle/hexagonal/controllers/PartnerControllerTest.java rename to infrastructure/src/test/java/br/com/fullcycle/infrastructure/rest/PartnerControllerTest.java index 13fdc03e..87b0f784 100644 --- a/src/test/java/br/com/fullcycle/hexagonal/controllers/PartnerControllerTest.java +++ b/infrastructure/src/test/java/br/com/fullcycle/infrastructure/rest/PartnerControllerTest.java @@ -1,7 +1,9 @@ -package br.com.fullcycle.hexagonal.controllers; +package br.com.fullcycle.infrastructure.rest; -import br.com.fullcycle.hexagonal.dtos.PartnerDTO; -import br.com.fullcycle.hexagonal.repositories.PartnerRepository; +import br.com.fullcycle.domain.partner.PartnerRepository; +import br.com.fullcycle.application.partner.CreatePartnerUseCase; +import br.com.fullcycle.application.partner.GetPartnerByIdUseCase; +import br.com.fullcycle.infrastructure.dtos.NewPartnerDTO; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; @@ -27,8 +29,8 @@ public class PartnerControllerTest { @Autowired private PartnerRepository partnerRepository; - @AfterEach - void tearDown() { + @BeforeEach + void setUP() { partnerRepository.deleteAll(); } @@ -36,10 +38,7 @@ void tearDown() { @DisplayName("Deve criar um parceiro") public void testCreate() throws Exception { - var partner = new PartnerDTO(); - partner.setCnpj("41536538000100"); - partner.setEmail("john.doe@gmail.com"); - partner.setName("John Doe"); + var partner = new NewPartnerDTO("41.536.538/0001-00", "john.doe@gmail.com", "John Doe"); final var result = this.mvc.perform( MockMvcRequestBuilders.post("/partners") @@ -48,23 +47,20 @@ public void testCreate() throws Exception { ) .andExpect(MockMvcResultMatchers.status().isCreated()) .andExpect(MockMvcResultMatchers.header().exists("Location")) - .andExpect(MockMvcResultMatchers.jsonPath("$.id").isNumber()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").isString()) .andReturn().getResponse().getContentAsByteArray(); - var actualResponse = mapper.readValue(result, PartnerDTO.class); - Assertions.assertEquals(partner.getName(), actualResponse.getName()); - Assertions.assertEquals(partner.getCnpj(), actualResponse.getCnpj()); - Assertions.assertEquals(partner.getEmail(), actualResponse.getEmail()); + var actualResponse = mapper.readValue(result, NewPartnerDTO.class); + Assertions.assertEquals(partner.name(), actualResponse.name()); + Assertions.assertEquals(partner.cnpj(), actualResponse.cnpj()); + Assertions.assertEquals(partner.email(), actualResponse.email()); } @Test @DisplayName("Não deve cadastrar um parceiro com CNPJ duplicado") public void testCreateWithDuplicatedCPFShouldFail() throws Exception { - var partner = new PartnerDTO(); - partner.setCnpj("41536538000100"); - partner.setEmail("john.doe@gmail.com"); - partner.setName("John Doe"); + var partner = new NewPartnerDTO("41.536.538/0001-00", "john.doe@gmail.com", "John Doe"); // Cria o primeiro parceiro this.mvc.perform( @@ -74,10 +70,10 @@ public void testCreateWithDuplicatedCPFShouldFail() throws Exception { ) .andExpect(MockMvcResultMatchers.status().isCreated()) .andExpect(MockMvcResultMatchers.header().exists("Location")) - .andExpect(MockMvcResultMatchers.jsonPath("$.id").isNumber()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").isString()) .andReturn().getResponse().getContentAsByteArray(); - partner.setEmail("john2@gmail.com"); + partner = new NewPartnerDTO("41.536.538/0001-00", "john2@gmail.com", "John Doe"); // Tenta criar o segundo parceiro com o mesmo CPF this.mvc.perform( @@ -93,10 +89,7 @@ public void testCreateWithDuplicatedCPFShouldFail() throws Exception { @DisplayName("Não deve cadastrar um parceiro com e-mail duplicado") public void testCreateWithDuplicatedEmailShouldFail() throws Exception { - var partner = new PartnerDTO(); - partner.setCnpj("41536538000100"); - partner.setEmail("john.doe@gmail.com"); - partner.setName("John Doe"); + var partner = new NewPartnerDTO("41.536.538/0001-00", "john.doe@gmail.com", "John Doe"); // Cria o primeiro parceiro this.mvc.perform( @@ -106,10 +99,10 @@ public void testCreateWithDuplicatedEmailShouldFail() throws Exception { ) .andExpect(MockMvcResultMatchers.status().isCreated()) .andExpect(MockMvcResultMatchers.header().exists("Location")) - .andExpect(MockMvcResultMatchers.jsonPath("$.id").isNumber()) + .andExpect(MockMvcResultMatchers.jsonPath("$.id").isString()) .andReturn().getResponse().getContentAsByteArray(); - partner.setCnpj("66666538000100"); + partner = new NewPartnerDTO("66.666.538/0001-00", "john.doe@gmail.com", "John Doe"); // Tenta criar o segundo parceiro com o mesmo CNPJ this.mvc.perform( @@ -125,10 +118,7 @@ public void testCreateWithDuplicatedEmailShouldFail() throws Exception { @DisplayName("Deve obter um parceiro por id") public void testGet() throws Exception { - var partner = new PartnerDTO(); - partner.setCnpj("41536538000100"); - partner.setEmail("john.doe@gmail.com"); - partner.setName("John Doe"); + var partner = new NewPartnerDTO("41.536.538/0001-00", "john.doe@gmail.com", "John Doe"); final var createResult = this.mvc.perform( MockMvcRequestBuilders.post("/partners") @@ -137,7 +127,7 @@ public void testGet() throws Exception { ) .andReturn().getResponse().getContentAsByteArray(); - var partnerId = mapper.readValue(createResult, PartnerDTO.class).getId(); + var partnerId = mapper.readValue(createResult, CreatePartnerUseCase.Output.class).id(); final var result = this.mvc.perform( MockMvcRequestBuilders.get("/partners/{id}", partnerId) @@ -145,10 +135,10 @@ public void testGet() throws Exception { .andExpect(MockMvcResultMatchers.status().isOk()) .andReturn().getResponse().getContentAsByteArray(); - var actualResponse = mapper.readValue(result, PartnerDTO.class); - Assertions.assertEquals(partnerId, actualResponse.getId()); - Assertions.assertEquals(partner.getName(), actualResponse.getName()); - Assertions.assertEquals(partner.getCnpj(), actualResponse.getCnpj()); - Assertions.assertEquals(partner.getEmail(), actualResponse.getEmail()); + var actualResponse = mapper.readValue(result, GetPartnerByIdUseCase.Output.class); + Assertions.assertEquals(partnerId, actualResponse.id()); + Assertions.assertEquals(partner.name(), actualResponse.name()); + Assertions.assertEquals(partner.cnpj(), actualResponse.cnpj()); + Assertions.assertEquals(partner.email(), actualResponse.email()); } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1194c105..54c9cb4f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,4 @@ rootProject.name = "mba-hexagonal-arch" +include("domain") +include("application") +include("infrastructure") diff --git a/src/main/java/br/com/fullcycle/hexagonal/controllers/CustomerController.java b/src/main/java/br/com/fullcycle/hexagonal/controllers/CustomerController.java deleted file mode 100644 index d8d9b300..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/controllers/CustomerController.java +++ /dev/null @@ -1,47 +0,0 @@ -package br.com.fullcycle.hexagonal.controllers; - -import br.com.fullcycle.hexagonal.dtos.CustomerDTO; -import br.com.fullcycle.hexagonal.models.Customer; -import br.com.fullcycle.hexagonal.services.CustomerService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.net.URI; - -@RestController -@RequestMapping(value = "customers") -public class CustomerController { - - @Autowired - private CustomerService customerService; - - @PostMapping - public ResponseEntity create(@RequestBody CustomerDTO dto) { - if (customerService.findByCpf(dto.getCpf()).isPresent()) { - return ResponseEntity.unprocessableEntity().body("Customer already exists"); - } - if (customerService.findByEmail(dto.getEmail()).isPresent()) { - return ResponseEntity.unprocessableEntity().body("Customer already exists"); - } - - var customer = new Customer(); - customer.setName(dto.getName()); - customer.setCpf(dto.getCpf()); - customer.setEmail(dto.getEmail()); - - customer = customerService.save(customer); - - return ResponseEntity.created(URI.create("/customers/" + customer.getId())).body(customer); - } - - @GetMapping("/{id}") - public ResponseEntity get(@PathVariable Long id) { - var customer = customerService.findById(id); - if (customer.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - return ResponseEntity.ok(customer.get()); - } -} \ No newline at end of file diff --git a/src/main/java/br/com/fullcycle/hexagonal/controllers/EventController.java b/src/main/java/br/com/fullcycle/hexagonal/controllers/EventController.java deleted file mode 100644 index 6e07e2d4..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/controllers/EventController.java +++ /dev/null @@ -1,90 +0,0 @@ -package br.com.fullcycle.hexagonal.controllers; - -import br.com.fullcycle.hexagonal.dtos.EventDTO; -import br.com.fullcycle.hexagonal.dtos.SubscribeDTO; -import br.com.fullcycle.hexagonal.models.Event; -import br.com.fullcycle.hexagonal.models.Ticket; -import br.com.fullcycle.hexagonal.models.TicketStatus; -import br.com.fullcycle.hexagonal.services.CustomerService; -import br.com.fullcycle.hexagonal.services.EventService; -import br.com.fullcycle.hexagonal.services.PartnerService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import java.time.Instant; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; - -import static org.springframework.http.HttpStatus.CREATED; - -@RestController -@RequestMapping(value = "events") -public class EventController { - - @Autowired - private CustomerService customerService; - - @Autowired - private EventService eventService; - - @Autowired - private PartnerService partnerService; - - @PostMapping - @ResponseStatus(CREATED) - public Event create(@RequestBody EventDTO dto) { - var event = new Event(); - event.setDate(LocalDate.parse(dto.getDate(), DateTimeFormatter.ISO_DATE)); - event.setName(dto.getName()); - event.setTotalSpots(dto.getTotalSpots()); - - var partner = partnerService.findById(dto.getPartner().getId()); - if (partner.isEmpty()) { - throw new RuntimeException("Partner not found"); - } - event.setPartner(partner.get()); - - return eventService.save(event); - } - - @Transactional - @PostMapping(value = "/{id}/subscribe") - public ResponseEntity subscribe(@PathVariable Long id, @RequestBody SubscribeDTO dto) { - - var maybeCustomer = customerService.findById(dto.getCustomerId()); - if (maybeCustomer.isEmpty()) { - return ResponseEntity.unprocessableEntity().body("Customer not found"); - } - - var maybeEvent = eventService.findById(id); - if (maybeEvent.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - var maybeTicket = eventService.findTicketByEventIdAndCustomerId(id, dto.getCustomerId()); - if (maybeTicket.isPresent()) { - return ResponseEntity.unprocessableEntity().body("Email already registered"); - } - - var customer = maybeCustomer.get(); - var event = maybeEvent.get(); - - if (event.getTotalSpots() < event.getTickets().size() + 1) { - throw new RuntimeException("Event sold out"); - } - - var ticket = new Ticket(); - ticket.setEvent(event); - ticket.setCustomer(customer); - ticket.setReservedAt(Instant.now()); - ticket.setStatus(TicketStatus.PENDING); - - event.getTickets().add(ticket); - - eventService.save(event); - - return ResponseEntity.ok(new EventDTO(event)); - } -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/controllers/PartnerController.java b/src/main/java/br/com/fullcycle/hexagonal/controllers/PartnerController.java deleted file mode 100644 index af61785b..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/controllers/PartnerController.java +++ /dev/null @@ -1,48 +0,0 @@ -package br.com.fullcycle.hexagonal.controllers; - -import br.com.fullcycle.hexagonal.dtos.PartnerDTO; -import br.com.fullcycle.hexagonal.models.Partner; -import br.com.fullcycle.hexagonal.services.PartnerService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.net.URI; - -@RestController -@RequestMapping(value = "partners") -public class PartnerController { - - @Autowired - private PartnerService partnerService; - - @PostMapping - public ResponseEntity create(@RequestBody PartnerDTO dto) { - if (partnerService.findByCnpj(dto.getCnpj()).isPresent()) { - return ResponseEntity.unprocessableEntity().body("Partner already exists"); - } - if (partnerService.findByEmail(dto.getEmail()).isPresent()) { - return ResponseEntity.unprocessableEntity().body("Partner already exists"); - } - - var partner = new Partner(); - partner.setName(dto.getName()); - partner.setCnpj(dto.getCnpj()); - partner.setEmail(dto.getEmail()); - - partner = partnerService.save(partner); - - return ResponseEntity.created(URI.create("/partners/" + partner.getId())).body(partner); - } - - @GetMapping("/{id}") - public ResponseEntity get(@PathVariable Long id) { - var partner = partnerService.findById(id); - if (partner.isEmpty()) { - return ResponseEntity.notFound().build(); - } - - return ResponseEntity.ok(partner.get()); - } - -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/dtos/CustomerDTO.java b/src/main/java/br/com/fullcycle/hexagonal/dtos/CustomerDTO.java deleted file mode 100644 index 0d6d9e72..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/dtos/CustomerDTO.java +++ /dev/null @@ -1,52 +0,0 @@ -package br.com.fullcycle.hexagonal.dtos; - -import br.com.fullcycle.hexagonal.models.Customer; - -public class CustomerDTO { - private Long id; - private String name; - private String cpf; - private String email; - - public CustomerDTO() { - } - - public CustomerDTO(Customer customer) { - this.id = customer.getId(); - this.name = customer.getName(); - this.cpf = customer.getCpf(); - this.email = customer.getEmail(); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getCpf() { - return cpf; - } - - public void setCpf(String cpf) { - this.cpf = cpf; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/dtos/EventDTO.java b/src/main/java/br/com/fullcycle/hexagonal/dtos/EventDTO.java deleted file mode 100644 index 300bfc4e..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/dtos/EventDTO.java +++ /dev/null @@ -1,66 +0,0 @@ -package br.com.fullcycle.hexagonal.dtos; - -import br.com.fullcycle.hexagonal.models.Event; - -import java.time.format.DateTimeFormatter; - -public class EventDTO { - - private Long id; - private String name; - private String date; - private int totalSpots; - private PartnerDTO partner; - - public EventDTO() { - } - - public EventDTO(Event event) { - this.id = event.getId(); - this.name = event.getName(); - this.date = event.getDate().format(DateTimeFormatter.ISO_DATE); - this.totalSpots = event.getTotalSpots(); - this.partner = new PartnerDTO(event.getPartner()); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getDate() { - return date; - } - - public void setDate(String date) { - this.date = date; - } - - public int getTotalSpots() { - return totalSpots; - } - - public void setTotalSpots(int totalSpots) { - this.totalSpots = totalSpots; - } - - public PartnerDTO getPartner() { - return partner; - } - - public void setPartner(PartnerDTO partner) { - this.partner = partner; - } - -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/dtos/PartnerDTO.java b/src/main/java/br/com/fullcycle/hexagonal/dtos/PartnerDTO.java deleted file mode 100644 index b28f9dab..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/dtos/PartnerDTO.java +++ /dev/null @@ -1,56 +0,0 @@ -package br.com.fullcycle.hexagonal.dtos; - -import br.com.fullcycle.hexagonal.models.Partner; - -public class PartnerDTO { - private Long id; - private String name; - private String cnpj; - private String email; - - public PartnerDTO() { - } - - public PartnerDTO(Long id) { - this.id = id; - } - - public PartnerDTO(Partner partner) { - this.id = partner.getId(); - this.name = partner.getName(); - this.cnpj = partner.getCnpj(); - this.email = partner.getEmail(); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getCnpj() { - return cnpj; - } - - public void setCnpj(String cnpj) { - this.cnpj = cnpj; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/dtos/SubscribeDTO.java b/src/main/java/br/com/fullcycle/hexagonal/dtos/SubscribeDTO.java deleted file mode 100644 index e6433fc6..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/dtos/SubscribeDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package br.com.fullcycle.hexagonal.dtos; - -public class SubscribeDTO { - - private Long customerId; - - public Long getCustomerId() { - return customerId; - } - - public void setCustomerId(Long customerId) { - this.customerId = customerId; - } -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/dtos/TicketDTO.java b/src/main/java/br/com/fullcycle/hexagonal/dtos/TicketDTO.java deleted file mode 100644 index f189a4fe..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/dtos/TicketDTO.java +++ /dev/null @@ -1,84 +0,0 @@ -package br.com.fullcycle.hexagonal.dtos; - -import br.com.fullcycle.hexagonal.models.Ticket; -import br.com.fullcycle.hexagonal.models.TicketStatus; - -import java.time.Instant; - -public class TicketDTO { - private Long id; - private int spot; - private CustomerDTO customer; - private EventDTO event; - private TicketStatus status; - private Instant paidAt; - private Instant reservedAt; - - public TicketDTO() { - } - - public TicketDTO(Ticket ticket) { - this.id = ticket.getId(); - this.customer = new CustomerDTO(ticket.getCustomer()); - this.event = new EventDTO(ticket.getEvent()); - this.status = ticket.getStatus(); - this.paidAt = ticket.getPaidAt(); - this.reservedAt = ticket.getReservedAt(); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public int getSpot() { - return spot; - } - - public void setSpot(int spot) { - this.spot = spot; - } - - public CustomerDTO getCustomer() { - return customer; - } - - public void setCustomer(CustomerDTO customer) { - this.customer = customer; - } - - public EventDTO getEvent() { - return event; - } - - public void setEvent(EventDTO event) { - this.event = event; - } - - public TicketStatus getStatus() { - return status; - } - - public void setStatus(TicketStatus status) { - this.status = status; - } - - public Instant getPaidAt() { - return paidAt; - } - - public void setPaidAt(Instant paidAt) { - this.paidAt = paidAt; - } - - public Instant getReservedAt() { - return reservedAt; - } - - public void setReservedAt(Instant reservedAt) { - this.reservedAt = reservedAt; - } -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/models/Event.java b/src/main/java/br/com/fullcycle/hexagonal/models/Event.java deleted file mode 100644 index 1e3a1358..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/models/Event.java +++ /dev/null @@ -1,104 +0,0 @@ -package br.com.fullcycle.hexagonal.models; - -import jakarta.persistence.*; - -import java.time.LocalDate; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -import static jakarta.persistence.GenerationType.IDENTITY; - -@Entity -@Table(name = "events") -public class Event { - - @Id - @GeneratedValue(strategy = IDENTITY) - private Long id; - - private String name; - - private LocalDate date; - - private int totalSpots; - - @ManyToOne(fetch = FetchType.LAZY) - private Partner partner; - - @OneToMany(cascade = CascadeType.ALL, mappedBy = "event") - private Set tickets; - - public Event() { - this.tickets = new HashSet<>(); - } - - public Event(Long id, String name, LocalDate date, int totalSpots, Set tickets) { - this.id = id; - this.name = name; - this.date = date; - this.totalSpots = totalSpots; - this.tickets = tickets != null ? tickets : new HashSet<>(); - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public LocalDate getDate() { - return date; - } - - public void setDate(LocalDate date) { - this.date = date; - } - - public int getTotalSpots() { - return totalSpots; - } - - public void setTotalSpots(int totalSpots) { - this.totalSpots = totalSpots; - } - - public Partner getPartner() { - return partner; - } - - public void setPartner(Partner partner) { - this.partner = partner; - } - - public Set getTickets() { - return tickets; - } - - public void setTickets(Set tickets) { - this.tickets = tickets; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Event event = (Event) o; - return Objects.equals(id, event.id); - } - - @Override - public int hashCode() { - return Objects.hash(id); - } -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/models/Partner.java b/src/main/java/br/com/fullcycle/hexagonal/models/Partner.java deleted file mode 100644 index fa3a13a4..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/models/Partner.java +++ /dev/null @@ -1,65 +0,0 @@ -package br.com.fullcycle.hexagonal.models; - -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.Table; - -import static jakarta.persistence.GenerationType.IDENTITY; - -@Entity -@Table(name = "partners") -public class Partner { - - @Id - @GeneratedValue(strategy = IDENTITY) - private Long id; - - private String name; - - private String cnpj; - - private String email; - - public Partner() { - } - - public Partner(Long id, String name, String cnpj, String email) { - this.id = id; - this.name = name; - this.cnpj = cnpj; - this.email = email; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getCnpj() { - return cnpj; - } - - public void setCnpj(String cnpj) { - this.cnpj = cnpj; - } - - public String getEmail() { - return email; - } - - public void setEmail(String email) { - this.email = email; - } -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/models/Ticket.java b/src/main/java/br/com/fullcycle/hexagonal/models/Ticket.java deleted file mode 100644 index 215412ef..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/models/Ticket.java +++ /dev/null @@ -1,103 +0,0 @@ -package br.com.fullcycle.hexagonal.models; - -import jakarta.persistence.*; - -import java.time.Instant; -import java.util.Objects; - -import static jakarta.persistence.GenerationType.IDENTITY; - -@Entity -@Table(name = "tickets") -public class Ticket { - - @Id - @GeneratedValue(strategy = IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - private Customer customer; - - @ManyToOne(fetch = FetchType.LAZY) - private Event event; - - @Enumerated(EnumType.STRING) - private TicketStatus status; - - private Instant paidAt; - - private Instant reservedAt; - - public Ticket() { - } - - public Ticket(Long id, Customer customer, Event event, TicketStatus status, Instant paidAt, Instant reservedAt) { - this.id = id; - this.customer = customer; - this.event = event; - this.status = status; - this.paidAt = paidAt; - this.reservedAt = reservedAt; - } - - public Long getId() { - return id; - } - - public void setId(Long id) { - this.id = id; - } - - public Customer getCustomer() { - return customer; - } - - public void setCustomer(Customer customer) { - this.customer = customer; - } - - public Event getEvent() { - return event; - } - - public void setEvent(Event event) { - this.event = event; - } - - public TicketStatus getStatus() { - return status; - } - - public void setStatus(TicketStatus status) { - this.status = status; - } - - public Instant getPaidAt() { - return paidAt; - } - - public void setPaidAt(Instant paidAt) { - this.paidAt = paidAt; - } - - public Instant getReservedAt() { - return reservedAt; - } - - public void setReservedAt(Instant reservedAt) { - this.reservedAt = reservedAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Ticket ticket = (Ticket) o; - return Objects.equals(customer, ticket.customer) && Objects.equals(event, ticket.event); - } - - @Override - public int hashCode() { - return Objects.hash(customer, event); - } -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/models/TicketStatus.java b/src/main/java/br/com/fullcycle/hexagonal/models/TicketStatus.java deleted file mode 100644 index c1aa6a4e..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/models/TicketStatus.java +++ /dev/null @@ -1,5 +0,0 @@ -package br.com.fullcycle.hexagonal.models; - -public enum TicketStatus { - PENDING, PROCESSING, PAID; -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/repositories/CustomerRepository.java b/src/main/java/br/com/fullcycle/hexagonal/repositories/CustomerRepository.java deleted file mode 100644 index ac87c0f0..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/repositories/CustomerRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package br.com.fullcycle.hexagonal.repositories; - -import br.com.fullcycle.hexagonal.models.Customer; -import org.springframework.data.repository.CrudRepository; - -import java.util.Optional; - -public interface CustomerRepository extends CrudRepository { - - Optional findByCpf(String cpf); - - Optional findByEmail(String email); -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/repositories/EventRepository.java b/src/main/java/br/com/fullcycle/hexagonal/repositories/EventRepository.java deleted file mode 100644 index fb28336c..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/repositories/EventRepository.java +++ /dev/null @@ -1,8 +0,0 @@ -package br.com.fullcycle.hexagonal.repositories; - -import br.com.fullcycle.hexagonal.models.Event; -import org.springframework.data.repository.CrudRepository; - -public interface EventRepository extends CrudRepository { - -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/repositories/PartnerRepository.java b/src/main/java/br/com/fullcycle/hexagonal/repositories/PartnerRepository.java deleted file mode 100644 index 0f09c735..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/repositories/PartnerRepository.java +++ /dev/null @@ -1,13 +0,0 @@ -package br.com.fullcycle.hexagonal.repositories; - -import br.com.fullcycle.hexagonal.models.Partner; -import org.springframework.data.repository.CrudRepository; - -import java.util.Optional; - -public interface PartnerRepository extends CrudRepository { - - Optional findByCnpj(String cnpj); - - Optional findByEmail(String email); -} \ No newline at end of file diff --git a/src/main/java/br/com/fullcycle/hexagonal/repositories/TicketRepository.java b/src/main/java/br/com/fullcycle/hexagonal/repositories/TicketRepository.java deleted file mode 100644 index c18133f8..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/repositories/TicketRepository.java +++ /dev/null @@ -1,11 +0,0 @@ -package br.com.fullcycle.hexagonal.repositories; - -import br.com.fullcycle.hexagonal.models.Ticket; -import org.springframework.data.repository.CrudRepository; - -import java.util.Optional; - -public interface TicketRepository extends CrudRepository { - - Optional findByEventIdAndCustomerId(Long id, Long customerId); -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/services/CustomerService.java b/src/main/java/br/com/fullcycle/hexagonal/services/CustomerService.java deleted file mode 100644 index 019a0587..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/services/CustomerService.java +++ /dev/null @@ -1,34 +0,0 @@ -package br.com.fullcycle.hexagonal.services; - -import br.com.fullcycle.hexagonal.models.Customer; -import br.com.fullcycle.hexagonal.repositories.CustomerRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@Service -public class CustomerService { - - @Autowired - private CustomerRepository repository; - - @Transactional - public Customer save(Customer customer) { - return repository.save(customer); - } - - public Optional findById(Long id) { - return repository.findById(id); - } - - public Optional findByCpf(String cpf) { - return repository.findByCpf(cpf); - } - - public Optional findByEmail(String email) { - return repository.findByEmail(email); - } - -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/services/EventService.java b/src/main/java/br/com/fullcycle/hexagonal/services/EventService.java deleted file mode 100644 index 6df6c747..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/services/EventService.java +++ /dev/null @@ -1,37 +0,0 @@ -package br.com.fullcycle.hexagonal.services; - -import br.com.fullcycle.hexagonal.models.Event; -import br.com.fullcycle.hexagonal.models.Ticket; -import br.com.fullcycle.hexagonal.repositories.EventRepository; -import br.com.fullcycle.hexagonal.repositories.TicketRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@Service -public class EventService { - - @Autowired - private CustomerService customerService; - - @Autowired - private EventRepository eventRepository; - - @Autowired - private TicketRepository ticketRepository; - - @Transactional - public Event save(Event event) { - return eventRepository.save(event); - } - - public Optional findById(Long id) { - return eventRepository.findById(id); - } - - public Optional findTicketByEventIdAndCustomerId(Long id, Long customerId) { - return ticketRepository.findByEventIdAndCustomerId(id, customerId); - } -} diff --git a/src/main/java/br/com/fullcycle/hexagonal/services/PartnerService.java b/src/main/java/br/com/fullcycle/hexagonal/services/PartnerService.java deleted file mode 100644 index 0f248775..00000000 --- a/src/main/java/br/com/fullcycle/hexagonal/services/PartnerService.java +++ /dev/null @@ -1,34 +0,0 @@ -package br.com.fullcycle.hexagonal.services; - -import br.com.fullcycle.hexagonal.models.Partner; -import br.com.fullcycle.hexagonal.repositories.PartnerRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.Optional; - -@Service -public class PartnerService { - - @Autowired - private PartnerRepository repository; - - @Transactional - public Partner save(Partner customer) { - return repository.save(customer); - } - - public Optional findById(Long id) { - return repository.findById(id); - } - - public Optional findByCnpj(String cnpj) { - return repository.findByCnpj(cnpj); - } - - public Optional findByEmail(String email) { - return repository.findByEmail(email); - } - -} diff --git a/src/test/java/br/com/fullcycle/hexagonal/controllers/EventControllerTest.java b/src/test/java/br/com/fullcycle/hexagonal/controllers/EventControllerTest.java deleted file mode 100644 index cea78058..00000000 --- a/src/test/java/br/com/fullcycle/hexagonal/controllers/EventControllerTest.java +++ /dev/null @@ -1,120 +0,0 @@ -package br.com.fullcycle.hexagonal.controllers; - -import br.com.fullcycle.hexagonal.dtos.EventDTO; -import br.com.fullcycle.hexagonal.dtos.PartnerDTO; -import br.com.fullcycle.hexagonal.dtos.SubscribeDTO; -import br.com.fullcycle.hexagonal.models.Customer; -import br.com.fullcycle.hexagonal.models.Partner; -import br.com.fullcycle.hexagonal.repositories.CustomerRepository; -import br.com.fullcycle.hexagonal.repositories.EventRepository; -import br.com.fullcycle.hexagonal.repositories.PartnerRepository; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.*; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.http.MediaType; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; -import org.springframework.test.web.servlet.result.MockMvcResultMatchers; -import org.springframework.transaction.annotation.Transactional; - -@ActiveProfiles("test") -@AutoConfigureMockMvc -@SpringBootTest -class EventControllerTest { - - @Autowired - private MockMvc mvc; - - @Autowired - private ObjectMapper mapper; - - @Autowired - private CustomerRepository customerRepository; - - @Autowired - private PartnerRepository partnerRepository; - - @Autowired - private EventRepository eventRepository; - - private Customer johnDoe; - private Partner disney; - - @BeforeEach - void setUp() { - johnDoe = customerRepository.save(new Customer(null, "John Doe", "123", "john@gmail.com")); - disney = partnerRepository.save(new Partner(null, "Disney", "456", "disney@gmail.com")); - } - - @AfterEach - void tearDown() { - eventRepository.deleteAll(); - customerRepository.deleteAll(); - partnerRepository.deleteAll(); - } - - @Test - @DisplayName("Deve criar um evento") - public void testCreate() throws Exception { - - var event = new EventDTO(); - event.setDate("2021-01-01"); - event.setName("Disney on Ice"); - event.setTotalSpots(100); - event.setPartner(new PartnerDTO(disney.getId())); - - final var result = this.mvc.perform( - MockMvcRequestBuilders.post("/events") - .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(event)) - ) - .andExpect(MockMvcResultMatchers.status().isCreated()) - .andExpect(MockMvcResultMatchers.jsonPath("$.id").isNumber()) - .andReturn().getResponse().getContentAsByteArray(); - - var actualResponse = mapper.readValue(result, EventDTO.class); - Assertions.assertEquals(event.getDate(), actualResponse.getDate()); - Assertions.assertEquals(event.getTotalSpots(), actualResponse.getTotalSpots()); - Assertions.assertEquals(event.getName(), actualResponse.getName()); - } - - @Test - @Transactional - @DisplayName("Deve comprar um ticket de um evento") - public void testReserveTicket() throws Exception { - - var event = new EventDTO(); - event.setDate("2021-01-01"); - event.setName("Disney on Ice"); - event.setTotalSpots(100); - event.setPartner(new PartnerDTO(disney.getId())); - - final var createResult = this.mvc.perform( - MockMvcRequestBuilders.post("/events") - .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(event)) - ) - .andExpect(MockMvcResultMatchers.status().isCreated()) - .andExpect(MockMvcResultMatchers.jsonPath("$.id").isNumber()) - .andReturn().getResponse().getContentAsByteArray(); - - var eventId = mapper.readValue(createResult, EventDTO.class).getId(); - - var sub = new SubscribeDTO(); - sub.setCustomerId(johnDoe.getId()); - - this.mvc.perform( - MockMvcRequestBuilders.post("/events/{id}/subscribe", eventId) - .contentType(MediaType.APPLICATION_JSON) - .content(mapper.writeValueAsString(sub)) - ) - .andExpect(MockMvcResultMatchers.status().isOk()) - .andReturn().getResponse().getContentAsByteArray(); - - var actualEvent = eventRepository.findById(eventId).get(); - Assertions.assertEquals(1, actualEvent.getTickets().size()); - } -} \ No newline at end of file