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 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 12:04:51 +01:00
parent a625e34fe4
commit fc77248c38
4 changed files with 28 additions and 19 deletions

View File

@@ -12,10 +12,10 @@ import de.fete.domain.model.CreateEventCommand;
import de.fete.domain.model.Event; import de.fete.domain.model.Event;
import de.fete.domain.model.EventToken; import de.fete.domain.model.EventToken;
import de.fete.domain.model.Rsvp; 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.CreateEventUseCase;
import de.fete.domain.port.in.CreateRsvpUseCase; import de.fete.domain.port.in.CreateRsvpUseCase;
import de.fete.domain.port.in.GetEventUseCase; import de.fete.domain.port.in.GetEventUseCase;
import de.fete.domain.port.out.RsvpRepository;
import java.time.Clock; import java.time.Clock;
import java.time.DateTimeException; import java.time.DateTimeException;
import java.time.LocalDate; import java.time.LocalDate;
@@ -32,20 +32,20 @@ public class EventController implements EventsApi {
private final CreateEventUseCase createEventUseCase; private final CreateEventUseCase createEventUseCase;
private final GetEventUseCase getEventUseCase; private final GetEventUseCase getEventUseCase;
private final CreateRsvpUseCase createRsvpUseCase; private final CreateRsvpUseCase createRsvpUseCase;
private final RsvpRepository rsvpRepository; private final CountAttendeesByEventUseCase countAttendeesByEventUseCase;
private final Clock clock; 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( public EventController(
CreateEventUseCase createEventUseCase, CreateEventUseCase createEventUseCase,
GetEventUseCase getEventUseCase, GetEventUseCase getEventUseCase,
CreateRsvpUseCase createRsvpUseCase, CreateRsvpUseCase createRsvpUseCase,
RsvpRepository rsvpRepository, CountAttendeesByEventUseCase countAttendeesByEventUseCase,
Clock clock) { Clock clock) {
this.createEventUseCase = createEventUseCase; this.createEventUseCase = createEventUseCase;
this.getEventUseCase = getEventUseCase; this.getEventUseCase = getEventUseCase;
this.createRsvpUseCase = createRsvpUseCase; this.createRsvpUseCase = createRsvpUseCase;
this.rsvpRepository = rsvpRepository; this.countAttendeesByEventUseCase = countAttendeesByEventUseCase;
this.clock = clock; this.clock = clock;
} }
@@ -90,7 +90,7 @@ public class EventController implements EventsApi {
response.setTimezone(event.getTimezone().getId()); response.setTimezone(event.getTimezone().getId());
response.setLocation(event.getLocation()); response.setLocation(event.getLocation());
response.setAttendeeCount( response.setAttendeeCount(
(int) rsvpRepository.countByEventId(event.getId())); (int) countAttendeesByEventUseCase.countByEvent(eventToken));
response.setExpired( response.setExpired(
event.getExpiryDate().isBefore(LocalDate.now(clock))); event.getExpiryDate().isBefore(LocalDate.now(clock)));

View File

@@ -4,28 +4,25 @@ import de.fete.domain.model.Event;
import de.fete.domain.model.EventToken; import de.fete.domain.model.EventToken;
import de.fete.domain.model.Rsvp; import de.fete.domain.model.Rsvp;
import de.fete.domain.model.RsvpToken; 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.in.CreateRsvpUseCase;
import de.fete.domain.port.out.EventRepository; import de.fete.domain.port.out.EventRepository;
import de.fete.domain.port.out.RsvpRepository; import de.fete.domain.port.out.RsvpRepository;
import java.time.Clock;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** Application service implementing RSVP creation. */ /** Application service implementing RSVP creation. */
@Service @Service
public class RsvpService implements CreateRsvpUseCase { public class RsvpService implements CreateRsvpUseCase, CountAttendeesByEventUseCase {
private final EventRepository eventRepository; private final EventRepository eventRepository;
private final RsvpRepository rsvpRepository; private final RsvpRepository rsvpRepository;
private final Clock clock;
/** Creates a new RsvpService. */ /** Creates a new RsvpService. */
public RsvpService( public RsvpService(
EventRepository eventRepository, EventRepository eventRepository,
RsvpRepository rsvpRepository, RsvpRepository rsvpRepository) {
Clock clock) {
this.eventRepository = eventRepository; this.eventRepository = eventRepository;
this.rsvpRepository = rsvpRepository; this.rsvpRepository = rsvpRepository;
this.clock = clock;
} }
@Override @Override
@@ -40,4 +37,11 @@ public class RsvpService implements CreateRsvpUseCase {
return rsvpRepository.save(rsvp); 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());
}
} }

View File

@@ -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);
}

View File

@@ -12,8 +12,6 @@ import de.fete.domain.model.OrganizerToken;
import de.fete.domain.model.Rsvp; import de.fete.domain.model.Rsvp;
import de.fete.domain.port.out.EventRepository; import de.fete.domain.port.out.EventRepository;
import de.fete.domain.port.out.RsvpRepository; import de.fete.domain.port.out.RsvpRepository;
import java.time.Clock;
import java.time.Instant;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.ZoneId; import java.time.ZoneId;
@@ -30,9 +28,6 @@ import org.mockito.junit.jupiter.MockitoExtension;
class RsvpServiceTest { class RsvpServiceTest {
private static final ZoneId ZONE = ZoneId.of("Europe/Berlin"); 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 @Mock
private EventRepository eventRepository; private EventRepository eventRepository;
@@ -44,7 +39,7 @@ class RsvpServiceTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
rsvpService = new RsvpService(eventRepository, rsvpRepository, FIXED_CLOCK); rsvpService = new RsvpService(eventRepository, rsvpRepository);
} }
@Test @Test
@@ -109,7 +104,7 @@ class RsvpServiceTest {
event.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2))); event.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)));
event.setTimezone(ZONE); event.setTimezone(ZONE);
event.setExpiryDate(LocalDate.of(2026, 7, 15)); event.setExpiryDate(LocalDate.of(2026, 7, 15));
event.setCreatedAt(OffsetDateTime.now(FIXED_CLOCK)); event.setCreatedAt(OffsetDateTime.now());
return event; return event;
} }
} }