Make expiryDate an internal concern, auto-set to event date + 7 days
All checks were successful
CI / backend-test (push) Successful in 58s
CI / frontend-test (push) Successful in 23s
CI / frontend-e2e (push) Successful in 1m12s
CI / build-and-publish (push) Has been skipped

The expiry date is no longer user-facing: it is removed from the API
(request and response) and the frontend. The backend now automatically
calculates it as the event date plus 7 days. The expired banner and
RSVP-bar filtering by expired status are also removed from the UI,
since expiry is purely an internal data-retention mechanism.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-09 21:29:12 +01:00
parent 6b3a06a72c
commit 0441ca0c33
18 changed files with 33 additions and 400 deletions

View File

@@ -20,9 +20,7 @@ import de.fete.domain.port.in.CreateEventUseCase;
import de.fete.domain.port.in.CreateRsvpUseCase;
import de.fete.domain.port.in.GetAttendeesUseCase;
import de.fete.domain.port.in.GetEventUseCase;
import java.time.Clock;
import java.time.DateTimeException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.List;
import java.util.UUID;
@@ -39,22 +37,19 @@ public class EventController implements EventsApi {
private final CreateRsvpUseCase createRsvpUseCase;
private final CountAttendeesByEventUseCase countAttendeesByEventUseCase;
private final GetAttendeesUseCase getAttendeesUseCase;
private final Clock clock;
/** Creates a new controller with the given use cases and clock. */
/** Creates a new controller with the given use cases. */
public EventController(
CreateEventUseCase createEventUseCase,
GetEventUseCase getEventUseCase,
CreateRsvpUseCase createRsvpUseCase,
CountAttendeesByEventUseCase countAttendeesByEventUseCase,
GetAttendeesUseCase getAttendeesUseCase,
Clock clock) {
GetAttendeesUseCase getAttendeesUseCase) {
this.createEventUseCase = createEventUseCase;
this.getEventUseCase = getEventUseCase;
this.createRsvpUseCase = createRsvpUseCase;
this.countAttendeesByEventUseCase = countAttendeesByEventUseCase;
this.getAttendeesUseCase = getAttendeesUseCase;
this.clock = clock;
}
@Override
@@ -67,8 +62,7 @@ public class EventController implements EventsApi {
request.getDescription(),
request.getDateTime(),
zoneId,
request.getLocation(),
request.getExpiryDate()
request.getLocation()
);
Event event = createEventUseCase.createEvent(command);
@@ -79,7 +73,6 @@ public class EventController implements EventsApi {
response.setTitle(event.getTitle());
response.setDateTime(event.getDateTime());
response.setTimezone(event.getTimezone().getId());
response.setExpiryDate(event.getExpiryDate());
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
@@ -99,8 +92,6 @@ public class EventController implements EventsApi {
response.setLocation(event.getLocation());
response.setAttendeeCount(
(int) countAttendeesByEventUseCase.countByEvent(eventToken));
response.setExpired(
event.getExpiryDate().isBefore(LocalDate.now(clock)));
return ResponseEntity.ok(response);
}

View File

@@ -17,6 +17,8 @@ import org.springframework.stereotype.Service;
@Service
public class EventService implements CreateEventUseCase, GetEventUseCase {
private static final int EXPIRY_DAYS_AFTER_EVENT = 7;
private final EventRepository eventRepository;
private final Clock clock;
@@ -28,13 +30,7 @@ public class EventService implements CreateEventUseCase, GetEventUseCase {
@Override
public Event createEvent(CreateEventCommand command) {
if (!command.expiryDate().isAfter(LocalDate.now(clock))) {
throw new ExpiryDateInPastException(command.expiryDate());
}
if (!command.expiryDate().isAfter(command.dateTime().toLocalDate())) {
throw new ExpiryDateBeforeEventException(command.expiryDate(), command.dateTime());
}
LocalDate expiryDate = command.dateTime().toLocalDate().plusDays(EXPIRY_DAYS_AFTER_EVENT);
var event = new Event();
event.setEventToken(EventToken.generate());
@@ -44,7 +40,7 @@ public class EventService implements CreateEventUseCase, GetEventUseCase {
event.setDateTime(command.dateTime());
event.setTimezone(command.timezone());
event.setLocation(command.location());
event.setExpiryDate(command.expiryDate());
event.setExpiryDate(expiryDate);
event.setCreatedAt(OffsetDateTime.now(clock));
return eventRepository.save(event);

View File

@@ -10,6 +10,5 @@ public record CreateEventCommand(
String description,
OffsetDateTime dateTime,
ZoneId timezone,
String location,
LocalDate expiryDate
String location
) {}

View File

@@ -160,7 +160,6 @@ components:
- title
- dateTime
- timezone
- expiryDate
properties:
title:
type: string
@@ -181,11 +180,6 @@ components:
location:
type: string
maxLength: 500
expiryDate:
type: string
format: date
description: Date after which event data is deleted. Must be in the future.
example: "2026-06-15"
CreateEventResponse:
type: object
@@ -195,7 +189,6 @@ components:
- title
- dateTime
- timezone
- expiryDate
properties:
eventToken:
type: string
@@ -218,10 +211,6 @@ components:
type: string
description: IANA timezone of the organizer
example: "Europe/Berlin"
expiryDate:
type: string
format: date
example: "2026-06-15"
GetEventResponse:
type: object
@@ -231,7 +220,6 @@ components:
- dateTime
- timezone
- attendeeCount
- expired
properties:
eventToken:
type: string
@@ -264,10 +252,6 @@ components:
minimum: 0
description: Number of confirmed attendees (attending=true)
example: 12
expired:
type: boolean
description: Whether the event's expiry date has passed
example: false
CreateRsvpRequest:
type: object

View File

@@ -55,8 +55,7 @@ class EventControllerIntegrationTest {
.description("Come celebrate!")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin")
.location("Berlin")
.expiryDate(LocalDate.of(2026, 6, 16));
.location("Berlin");
var result = mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
@@ -67,7 +66,6 @@ class EventControllerIntegrationTest {
.andExpect(jsonPath("$.title").value("Birthday Party"))
.andExpect(jsonPath("$.timezone").value("Europe/Berlin"))
.andExpect(jsonPath("$.dateTime").isNotEmpty())
.andExpect(jsonPath("$.expiryDate").isNotEmpty())
.andReturn();
var response = objectMapper.readValue(
@@ -79,7 +77,7 @@ class EventControllerIntegrationTest {
assertThat(persisted.getDescription()).isEqualTo("Come celebrate!");
assertThat(persisted.getTimezone()).isEqualTo("Europe/Berlin");
assertThat(persisted.getLocation()).isEqualTo("Berlin");
assertThat(persisted.getExpiryDate()).isEqualTo(request.getExpiryDate());
assertThat(persisted.getExpiryDate()).isEqualTo(LocalDate.of(2026, 6, 22));
assertThat(persisted.getDateTime().toInstant())
.isEqualTo(request.getDateTime().toInstant());
assertThat(persisted.getOrganizerToken()).isNotNull();
@@ -91,8 +89,7 @@ class EventControllerIntegrationTest {
var request = new CreateEventRequest()
.title("Minimal Event")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("UTC")
.expiryDate(LocalDate.of(2026, 6, 16));
.timezone("UTC");
var result = mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
@@ -119,8 +116,7 @@ class EventControllerIntegrationTest {
var request = new CreateEventRequest()
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin")
.expiryDate(LocalDate.of(2026, 6, 16));
.timezone("Europe/Berlin");
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
@@ -139,26 +135,6 @@ class EventControllerIntegrationTest {
var request = new CreateEventRequest()
.title("No Date")
.timezone("Europe/Berlin")
.expiryDate(LocalDate.of(2026, 6, 16));
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.fieldErrors").isArray());
assertThat(jpaRepository.count()).isEqualTo(countBefore);
}
@Test
void createEventMissingExpiryDateReturns400() throws Exception {
long countBefore = jpaRepository.count();
var request = new CreateEventRequest()
.title("No Expiry")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin");
mockMvc.perform(post("/api/events")
@@ -171,93 +147,12 @@ class EventControllerIntegrationTest {
assertThat(jpaRepository.count()).isEqualTo(countBefore);
}
@Test
void createEventExpiryDateInPastReturns400() throws Exception {
long countBefore = jpaRepository.count();
var request = new CreateEventRequest()
.title("Past Expiry")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin")
.expiryDate(LocalDate.of(2025, 1, 1));
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:expiry-date-in-past"));
assertThat(jpaRepository.count()).isEqualTo(countBefore);
}
@Test
void createEventExpiryDateTodayReturns400() throws Exception {
long countBefore = jpaRepository.count();
var request = new CreateEventRequest()
.title("Today Expiry")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin")
.expiryDate(LocalDate.now());
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:expiry-date-in-past"));
assertThat(jpaRepository.count()).isEqualTo(countBefore);
}
@Test
void createEventExpiryDateBeforeEventDateReturns400() throws Exception {
long countBefore = jpaRepository.count();
var request = new CreateEventRequest()
.title("Bad Expiry")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin")
.expiryDate(LocalDate.of(2026, 6, 10));
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:expiry-date-before-event"));
assertThat(jpaRepository.count()).isEqualTo(countBefore);
}
@Test
void createEventExpiryDateSameAsEventDateReturns400() throws Exception {
long countBefore = jpaRepository.count();
var request = new CreateEventRequest()
.title("Same Day Expiry")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin")
.expiryDate(LocalDate.of(2026, 6, 15));
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.type").value("urn:problem-type:expiry-date-before-event"));
assertThat(jpaRepository.count()).isEqualTo(countBefore);
}
@Test
void errorResponseContentTypeIsProblemJson() throws Exception {
var request = new CreateEventRequest()
.title("")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Europe/Berlin")
.expiryDate(LocalDate.of(2026, 6, 16));
.timezone("Europe/Berlin");
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
@@ -273,8 +168,7 @@ class EventControllerIntegrationTest {
var request = new CreateEventRequest()
.title("Bad TZ")
.dateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)))
.timezone("Not/A/Zone")
.expiryDate(LocalDate.of(2026, 6, 16));
.timezone("Not/A/Zone");
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON)
@@ -302,7 +196,6 @@ class EventControllerIntegrationTest {
.andExpect(jsonPath("$.timezone").value("Europe/Berlin"))
.andExpect(jsonPath("$.location").value("Central Park"))
.andExpect(jsonPath("$.attendeeCount").value(0))
.andExpect(jsonPath("$.expired").value(false))
.andExpect(jsonPath("$.dateTime").isNotEmpty());
}
@@ -327,18 +220,6 @@ class EventControllerIntegrationTest {
.andExpect(jsonPath("$.type").value("urn:problem-type:event-not-found"));
}
@Test
void getExpiredEventReturnsExpiredTrue() throws Exception {
EventJpaEntity entity = seedEvent(
"Past Event", "It happened", "Europe/Berlin",
"Old Venue", LocalDate.now().minusDays(1));
mockMvc.perform(get("/api/events/" + entity.getEventToken()))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("Past Event"))
.andExpect(jsonPath("$.expired").value(true));
}
// --- RSVP tests ---
@Test

View File

@@ -1,7 +1,6 @@
package de.fete.application.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -53,8 +52,7 @@ class EventServiceTest {
"Come celebrate!",
TODAY.plusDays(90).atStartOfDay(ZONE).toOffsetDateTime(),
ZONE,
"Berlin",
TODAY.plusDays(120)
"Berlin"
);
Event result = eventService.createEvent(command);
@@ -75,8 +73,7 @@ class EventServiceTest {
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null,
TODAY.plusDays(11)
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null
);
eventService.createEvent(command);
@@ -87,86 +84,19 @@ class EventServiceTest {
}
@Test
void expiryDateTodayThrowsException() {
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null,
TODAY
);
assertThatThrownBy(() -> eventService.createEvent(command))
.isInstanceOf(ExpiryDateInPastException.class);
}
@Test
void expiryDateInPastThrowsException() {
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null,
TODAY.minusDays(5)
);
assertThatThrownBy(() -> eventService.createEvent(command))
.isInstanceOf(ExpiryDateInPastException.class);
}
@Test
void expiryDateTomorrowSucceeds() {
void expiryDateIsEventDatePlusSevenDays() {
when(eventRepository.save(any(Event.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
var eventDate = TODAY.plusDays(10);
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(1).atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null,
TODAY.plusDays(2)
eventDate.atStartOfDay(ZONE).toOffsetDateTime(), ZONE, null
);
Event result = eventService.createEvent(command);
assertThat(result.getExpiryDate()).isEqualTo(TODAY.plusDays(2));
}
@Test
void expiryDateSameAsEventDateThrowsException() {
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(),
ZONE, null,
TODAY.plusDays(10)
);
assertThatThrownBy(() -> eventService.createEvent(command))
.isInstanceOf(ExpiryDateBeforeEventException.class);
}
@Test
void expiryDateBeforeEventDateThrowsException() {
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(),
ZONE, null,
TODAY.plusDays(5)
);
assertThatThrownBy(() -> eventService.createEvent(command))
.isInstanceOf(ExpiryDateBeforeEventException.class);
}
@Test
void expiryDateDayAfterEventDateSucceeds() {
when(eventRepository.save(any(Event.class)))
.thenAnswer(invocation -> invocation.getArgument(0));
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(),
ZONE, null,
TODAY.plusDays(11)
);
Event result = eventService.createEvent(command);
assertThat(result.getExpiryDate()).isEqualTo(TODAY.plusDays(11));
assertThat(result.getExpiryDate()).isEqualTo(eventDate.plusDays(7));
}
// --- GetEventUseCase tests (T004) ---
@@ -207,8 +137,7 @@ class EventServiceTest {
var command = new CreateEventCommand(
"Test", null,
TODAY.plusDays(10).atStartOfDay(ZONE).toOffsetDateTime(),
ZoneId.of("America/New_York"), null,
TODAY.plusDays(11)
ZoneId.of("America/New_York"), null
);
Event result = eventService.createEvent(command);