Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
dfc9cc5
01-vamos-refatorar-um-mini-projeto-aplicando-ports-and-adapters
deploydesexta Aug 13, 2023
b9ae313
02-anatomia-de-um-caso-de-uso
deploydesexta Aug 13, 2023
4c0d3ef
03-extraindo-o-caso-de-uso-createcustomer
deploydesexta Aug 13, 2023
01b6b44
04-extraindo-os-casos-de-uso-implicitos
deploydesexta Aug 13, 2023
2c43fc5
05-casos-de-uso-relacionados-aos-eventos-e-ingressos
deploydesexta Aug 13, 2023
e2a7400
06-testes-unitarios-dos-casos-de-uso
deploydesexta Aug 13, 2023
e3acd03
07-injecao-de-dependencia-nos-casos-de-uso-e-testes-integrados
deploydesexta Aug 13, 2023
ac2c33b
08-refatorando-os-adapters-dos-drivers
deploydesexta Aug 13, 2023
645cf1f
09-modelando-as-entidades-de-dominio-parte-1
deploydesexta Aug 14, 2023
4d82dca
10-modelando-as-entidades-de-dominio-parte-1-e-2
deploydesexta Aug 14, 2023
367164c
11-modelando-as-entidades-de-dominio-parte-3
deploydesexta Aug 20, 2023
637f0e0
12-modelando-as-entidades-de-dominio-parte-4
deploydesexta Aug 20, 2023
42cff77
13-modelando-as-entidades-de-dominio-consideracoes-finais-e-testes-de…
deploydesexta Aug 21, 2023
a6f40db
14-interface-adapters-de-customer-partner-and-ticket
deploydesexta Aug 21, 2023
b253500
15-interface-adapters-de-event
deploydesexta Aug 26, 2023
b961638
01-segregando-as-camadas-de-domain-e-application
deploydesexta Sep 4, 2023
9f77293
02-segregando-a-camada-de-infrastructure
deploydesexta Sep 4, 2023
c3e4ea5
03-adicionado-suporte-ao-padrao-presenters
deploydesexta Sep 4, 2023
9705a3a
04-separando-frameworks-and-drivers-de-interface-adapters
deploydesexta Sep 4, 2023
547f81d
05-aplicando-eventos-de-dominio-para-desacoplar-agregados
deploydesexta Sep 4, 2023
7b317f7
06-event-driven-architecture-para-finalizar-o-fluxo
deploydesexta Sep 4, 2023
1e3f84d
initial commit
joaopdmota Aug 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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`.
10 changes: 10 additions & 0 deletions application/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
plugins {
`java-conventions`
`java-library`
}

group = "br.com.fullcycle.application"

dependencies {
implementation(project(":domain"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package br.com.fullcycle.application;

public abstract class NullaryUseCase<OUTPUT> {

// 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> T execute(Presenter<OUTPUT, T> presenter) {
try {
return presenter.present(execute());
} catch (Throwable t) {
return presenter.present(t);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package br.com.fullcycle.application;

public interface Presenter<IN, OUT> {

OUT present(IN input);

OUT present(Throwable error);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package br.com.fullcycle.application;

public abstract class UnitUseCase<INPUT> {

// 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);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package br.com.fullcycle.application;

public abstract class UseCase<INPUT, OUTPUT> {

// 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> T execute(INPUT input, Presenter<OUTPUT, T> presenter) {
try {
return presenter.present(execute(input));
} catch (Throwable t) {
return presenter.present(t);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<CreateCustomerUseCase.Input, CreateCustomerUseCase.Output> {

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) {
}
}
Original file line number Diff line number Diff line change
@@ -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<GetCustomerByIdUseCase.Input, Optional<GetCustomerByIdUseCase.Output>> {

private final CustomerRepository customerRepository;

public GetCustomerByIdUseCase(final CustomerRepository customerRepository) {
this.customerRepository = Objects.requireNonNull(customerRepository);
}

@Override
public Optional<Output> 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) {
}
}
Original file line number Diff line number Diff line change
@@ -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<CancelEventUseCase.Input, CancelEventUseCase.Output> {

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) {
}
}
Original file line number Diff line number Diff line change
@@ -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<CreateEventUseCase.Input, CreateEventUseCase.Output> {

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) {
}
}
Original file line number Diff line number Diff line change
@@ -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<GetEventByIdUseCase.Input, Optional<GetEventByIdUseCase.Output>> {

private final EventRepository eventRepository;

public GetEventByIdUseCase(final EventRepository eventRepository) {
this.eventRepository = Objects.requireNonNull(eventRepository);
}

@Override
public Optional<Output> 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) {
}
}
Original file line number Diff line number Diff line change
@@ -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<SubscribeCustomerToEventUseCase.Input, SubscribeCustomerToEventUseCase.Output> {

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) {
}
}
Loading