diff --git a/backend/src/main/java/de/fete/adapter/in/web/GlobalExceptionHandler.java b/backend/src/main/java/de/fete/adapter/in/web/GlobalExceptionHandler.java index 34c9726..33e3f24 100644 --- a/backend/src/main/java/de/fete/adapter/in/web/GlobalExceptionHandler.java +++ b/backend/src/main/java/de/fete/adapter/in/web/GlobalExceptionHandler.java @@ -1,5 +1,6 @@ package de.fete.adapter.in.web; +import de.fete.application.service.EventExpiredException; import de.fete.application.service.EventNotFoundException; import de.fete.application.service.ExpiryDateInPastException; import de.fete.application.service.InvalidTimezoneException; @@ -59,6 +60,19 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { .body(problemDetail); } + /** Handles RSVP on expired event. */ + @ExceptionHandler(EventExpiredException.class) + public ResponseEntity handleEventExpired( + EventExpiredException ex) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( + HttpStatus.CONFLICT, ex.getMessage()); + problemDetail.setTitle("Event Expired"); + problemDetail.setType(URI.create("urn:problem-type:event-expired")); + return ResponseEntity.status(HttpStatus.CONFLICT) + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .body(problemDetail); + } + /** Handles event not found. */ @ExceptionHandler(EventNotFoundException.class) public ResponseEntity handleEventNotFound( 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 65cfe19..0790d17 100644 --- a/backend/src/main/java/de/fete/application/service/RsvpService.java +++ b/backend/src/main/java/de/fete/application/service/RsvpService.java @@ -8,6 +8,8 @@ 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 java.time.LocalDate; import org.springframework.stereotype.Service; /** Application service implementing RSVP creation. */ @@ -16,13 +18,16 @@ public class RsvpService implements CreateRsvpUseCase, CountAttendeesByEventUseC private final EventRepository eventRepository; private final RsvpRepository rsvpRepository; + private final Clock clock; /** Creates a new RsvpService. */ public RsvpService( EventRepository eventRepository, - RsvpRepository rsvpRepository) { + RsvpRepository rsvpRepository, + Clock clock) { this.eventRepository = eventRepository; this.rsvpRepository = rsvpRepository; + this.clock = clock; } @Override @@ -30,6 +35,10 @@ public class RsvpService implements CreateRsvpUseCase, CountAttendeesByEventUseC Event event = eventRepository.findByEventToken(eventToken) .orElseThrow(() -> new EventNotFoundException(eventToken.value())); + if (!event.getExpiryDate().isAfter(LocalDate.now(clock))) { + throw new EventExpiredException(eventToken.value()); + } + var rsvp = new Rsvp(); rsvp.setRsvpToken(RsvpToken.generate()); rsvp.setEventId(event.getId()); diff --git a/backend/src/test/java/de/fete/adapter/in/web/EventControllerIntegrationTest.java b/backend/src/test/java/de/fete/adapter/in/web/EventControllerIntegrationTest.java index 70c8518..ed1242a 100644 --- a/backend/src/test/java/de/fete/adapter/in/web/EventControllerIntegrationTest.java +++ b/backend/src/test/java/de/fete/adapter/in/web/EventControllerIntegrationTest.java @@ -349,6 +349,22 @@ class EventControllerIntegrationTest { .andExpect(jsonPath("$.type").value("urn:problem-type:event-not-found")); } + @Test + void createRsvpForExpiredEventReturns409() throws Exception { + EventJpaEntity event = seedEvent( + "Expired Party", null, "Europe/Berlin", + null, LocalDate.now().minusDays(1)); + + var request = new CreateRsvpRequest().name("Late Guest"); + + mockMvc.perform(post("/api/events/" + event.getEventToken() + "/rsvps") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andExpect(status().isConflict()) + .andExpect(content().contentTypeCompatibleWith("application/problem+json")) + .andExpect(jsonPath("$.type").value("urn:problem-type:event-expired")); + } + private EventJpaEntity seedEvent( String title, String description, String timezone, String location, LocalDate expiryDate) { 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 6503e1f..21e9296 100644 --- a/backend/src/test/java/de/fete/application/service/RsvpServiceTest.java +++ b/backend/src/test/java/de/fete/application/service/RsvpServiceTest.java @@ -12,6 +12,8 @@ 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; @@ -28,6 +30,9 @@ import org.mockito.junit.jupiter.MockitoExtension; class RsvpServiceTest { private static final ZoneId ZONE = ZoneId.of("Europe/Berlin"); + private static final Instant NOW = Instant.parse("2026-03-08T12:00:00Z"); + private static final Clock FIXED_CLOCK = Clock.fixed(NOW, ZONE); + private static final LocalDate TODAY = LocalDate.ofInstant(NOW, ZONE); @Mock private EventRepository eventRepository; @@ -39,7 +44,7 @@ class RsvpServiceTest { @BeforeEach void setUp() { - rsvpService = new RsvpService(eventRepository, rsvpRepository); + rsvpService = new RsvpService(eventRepository, rsvpRepository, FIXED_CLOCK); } @Test @@ -95,6 +100,28 @@ class RsvpServiceTest { assertThat(result.getName()).isEqualTo("Max"); } + @Test + void createRsvpThrowsWhenEventExpired() { + var event = buildActiveEvent(); + event.setExpiryDate(TODAY.minusDays(1)); + EventToken token = event.getEventToken(); + when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event)); + + assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest")) + .isInstanceOf(EventExpiredException.class); + } + + @Test + void createRsvpThrowsWhenEventExpiresToday() { + var event = buildActiveEvent(); + event.setExpiryDate(TODAY); + EventToken token = event.getEventToken(); + when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event)); + + assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest")) + .isInstanceOf(EventExpiredException.class); + } + private Event buildActiveEvent() { var event = new Event(); event.setId(1L); @@ -103,7 +130,7 @@ class RsvpServiceTest { event.setTitle("Test Event"); 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.setExpiryDate(TODAY.plusDays(30)); event.setCreatedAt(OffsetDateTime.now()); return event; }