From fc77248c38a20ae33fed6c84a2eaf48c027a184b Mon Sep 17 00:00:00 2001 From: nitrix Date: Sun, 8 Mar 2026 12:04:51 +0100 Subject: [PATCH] Extract CountAttendeesByEventUseCase to decouple controller from repository The EventController was directly accessing RsvpRepository (an outbound port) to count attendees, bypassing the application layer. Introduce a dedicated inbound port and implement it in RsvpService. Remove the now-unused Clock dependency from RsvpService. Co-Authored-By: Claude Opus 4.6 --- .../de/fete/adapter/in/web/EventController.java | 12 ++++++------ .../de/fete/application/service/RsvpService.java | 16 ++++++++++------ .../port/in/CountAttendeesByEventUseCase.java | 10 ++++++++++ .../application/service/RsvpServiceTest.java | 9 ++------- 4 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 backend/src/main/java/de/fete/domain/port/in/CountAttendeesByEventUseCase.java diff --git a/backend/src/main/java/de/fete/adapter/in/web/EventController.java b/backend/src/main/java/de/fete/adapter/in/web/EventController.java index e103ab8..b7c828d 100644 --- a/backend/src/main/java/de/fete/adapter/in/web/EventController.java +++ b/backend/src/main/java/de/fete/adapter/in/web/EventController.java @@ -12,10 +12,10 @@ import de.fete.domain.model.CreateEventCommand; import de.fete.domain.model.Event; import de.fete.domain.model.EventToken; import de.fete.domain.model.Rsvp; +import de.fete.domain.port.in.CountAttendeesByEventUseCase; import de.fete.domain.port.in.CreateEventUseCase; import de.fete.domain.port.in.CreateRsvpUseCase; import de.fete.domain.port.in.GetEventUseCase; -import de.fete.domain.port.out.RsvpRepository; import java.time.Clock; import java.time.DateTimeException; import java.time.LocalDate; @@ -32,20 +32,20 @@ public class EventController implements EventsApi { private final CreateEventUseCase createEventUseCase; private final GetEventUseCase getEventUseCase; private final CreateRsvpUseCase createRsvpUseCase; - private final RsvpRepository rsvpRepository; + private final CountAttendeesByEventUseCase countAttendeesByEventUseCase; private final Clock clock; - /** Creates a new controller with the given use cases, repository, and clock. */ + /** Creates a new controller with the given use cases and clock. */ public EventController( CreateEventUseCase createEventUseCase, GetEventUseCase getEventUseCase, CreateRsvpUseCase createRsvpUseCase, - RsvpRepository rsvpRepository, + CountAttendeesByEventUseCase countAttendeesByEventUseCase, Clock clock) { this.createEventUseCase = createEventUseCase; this.getEventUseCase = getEventUseCase; this.createRsvpUseCase = createRsvpUseCase; - this.rsvpRepository = rsvpRepository; + this.countAttendeesByEventUseCase = countAttendeesByEventUseCase; this.clock = clock; } @@ -90,7 +90,7 @@ public class EventController implements EventsApi { response.setTimezone(event.getTimezone().getId()); response.setLocation(event.getLocation()); response.setAttendeeCount( - (int) rsvpRepository.countByEventId(event.getId())); + (int) countAttendeesByEventUseCase.countByEvent(eventToken)); response.setExpired( event.getExpiryDate().isBefore(LocalDate.now(clock))); diff --git a/backend/src/main/java/de/fete/application/service/RsvpService.java b/backend/src/main/java/de/fete/application/service/RsvpService.java index 5153a24..65cfe19 100644 --- a/backend/src/main/java/de/fete/application/service/RsvpService.java +++ b/backend/src/main/java/de/fete/application/service/RsvpService.java @@ -4,28 +4,25 @@ import de.fete.domain.model.Event; import de.fete.domain.model.EventToken; import de.fete.domain.model.Rsvp; import de.fete.domain.model.RsvpToken; +import de.fete.domain.port.in.CountAttendeesByEventUseCase; import de.fete.domain.port.in.CreateRsvpUseCase; import de.fete.domain.port.out.EventRepository; import de.fete.domain.port.out.RsvpRepository; -import java.time.Clock; import org.springframework.stereotype.Service; /** Application service implementing RSVP creation. */ @Service -public class RsvpService implements CreateRsvpUseCase { +public class RsvpService implements CreateRsvpUseCase, CountAttendeesByEventUseCase { private final EventRepository eventRepository; private final RsvpRepository rsvpRepository; - private final Clock clock; /** Creates a new RsvpService. */ public RsvpService( EventRepository eventRepository, - RsvpRepository rsvpRepository, - Clock clock) { + RsvpRepository rsvpRepository) { this.eventRepository = eventRepository; this.rsvpRepository = rsvpRepository; - this.clock = clock; } @Override @@ -40,4 +37,11 @@ public class RsvpService implements CreateRsvpUseCase { return rsvpRepository.save(rsvp); } + + @Override + public long countByEvent(EventToken eventToken) { + Event event = eventRepository.findByEventToken(eventToken) + .orElseThrow(() -> new EventNotFoundException(eventToken.value())); + return rsvpRepository.countByEventId(event.getId()); + } } diff --git a/backend/src/main/java/de/fete/domain/port/in/CountAttendeesByEventUseCase.java b/backend/src/main/java/de/fete/domain/port/in/CountAttendeesByEventUseCase.java new file mode 100644 index 0000000..91c7c96 --- /dev/null +++ b/backend/src/main/java/de/fete/domain/port/in/CountAttendeesByEventUseCase.java @@ -0,0 +1,10 @@ +package de.fete.domain.port.in; + +import de.fete.domain.model.EventToken; + +/** Inbound port for counting attendees of an event. */ +public interface CountAttendeesByEventUseCase { + + /** Counts the number of confirmed attendees for the given event. */ + long countByEvent(EventToken eventToken); +} diff --git a/backend/src/test/java/de/fete/application/service/RsvpServiceTest.java b/backend/src/test/java/de/fete/application/service/RsvpServiceTest.java index d5f6f36..6503e1f 100644 --- a/backend/src/test/java/de/fete/application/service/RsvpServiceTest.java +++ b/backend/src/test/java/de/fete/application/service/RsvpServiceTest.java @@ -12,8 +12,6 @@ import de.fete.domain.model.OrganizerToken; import de.fete.domain.model.Rsvp; import de.fete.domain.port.out.EventRepository; import de.fete.domain.port.out.RsvpRepository; -import java.time.Clock; -import java.time.Instant; import java.time.LocalDate; import java.time.OffsetDateTime; import java.time.ZoneId; @@ -30,9 +28,6 @@ import org.mockito.junit.jupiter.MockitoExtension; class RsvpServiceTest { private static final ZoneId ZONE = ZoneId.of("Europe/Berlin"); - private static final Instant FIXED_INSTANT = - LocalDate.of(2026, 3, 5).atStartOfDay(ZONE).toInstant(); - private static final Clock FIXED_CLOCK = Clock.fixed(FIXED_INSTANT, ZONE); @Mock private EventRepository eventRepository; @@ -44,7 +39,7 @@ class RsvpServiceTest { @BeforeEach void setUp() { - rsvpService = new RsvpService(eventRepository, rsvpRepository, FIXED_CLOCK); + rsvpService = new RsvpService(eventRepository, rsvpRepository); } @Test @@ -109,7 +104,7 @@ class RsvpServiceTest { event.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2))); event.setTimezone(ZONE); event.setExpiryDate(LocalDate.of(2026, 7, 15)); - event.setCreatedAt(OffsetDateTime.now(FIXED_CLOCK)); + event.setCreatedAt(OffsetDateTime.now()); return event; } }