Add RSVP feature: submit RSVP, block on expired events #16
@@ -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<ProblemDetail> 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<ProblemDetail> handleEventNotFound(
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user