Implement GET /events/{token} backend with timezone support
Domain: add timezone field to Event and CreateEventCommand. Ports: new GetEventUseCase inbound port. Service: implement getByEventToken, validate IANA timezone on create. Controller: map to GetEventResponse, compute expired flag via Clock. Persistence: timezone column in JPA entity and mapping. Tests: integration tests use DTOs + ObjectMapper instead of inline JSON, GET tests seed DB directly via JpaRepository for isolation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,18 @@ package de.fete.adapter.in.web;
|
||||
import de.fete.adapter.in.web.api.EventsApi;
|
||||
import de.fete.adapter.in.web.model.CreateEventRequest;
|
||||
import de.fete.adapter.in.web.model.CreateEventResponse;
|
||||
import de.fete.adapter.in.web.model.GetEventResponse;
|
||||
import de.fete.application.service.EventNotFoundException;
|
||||
import de.fete.application.service.InvalidTimezoneException;
|
||||
import de.fete.domain.model.CreateEventCommand;
|
||||
import de.fete.domain.model.Event;
|
||||
import de.fete.domain.port.in.CreateEventUseCase;
|
||||
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.UUID;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@@ -15,19 +24,29 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
public class EventController implements EventsApi {
|
||||
|
||||
private final CreateEventUseCase createEventUseCase;
|
||||
private final GetEventUseCase getEventUseCase;
|
||||
private final Clock clock;
|
||||
|
||||
/** Creates a new controller with the given use case. */
|
||||
public EventController(CreateEventUseCase createEventUseCase) {
|
||||
/** Creates a new controller with the given use cases and clock. */
|
||||
public EventController(
|
||||
CreateEventUseCase createEventUseCase,
|
||||
GetEventUseCase getEventUseCase,
|
||||
Clock clock) {
|
||||
this.createEventUseCase = createEventUseCase;
|
||||
this.getEventUseCase = getEventUseCase;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<CreateEventResponse> createEvent(
|
||||
CreateEventRequest request) {
|
||||
ZoneId zoneId = parseTimezone(request.getTimezone());
|
||||
|
||||
var command = new CreateEventCommand(
|
||||
request.getTitle(),
|
||||
request.getDescription(),
|
||||
request.getDateTime(),
|
||||
zoneId,
|
||||
request.getLocation(),
|
||||
request.getExpiryDate()
|
||||
);
|
||||
@@ -39,8 +58,36 @@ public class EventController implements EventsApi {
|
||||
response.setOrganizerToken(event.getOrganizerToken());
|
||||
response.setTitle(event.getTitle());
|
||||
response.setDateTime(event.getDateTime());
|
||||
response.setTimezone(event.getTimezone().getId());
|
||||
response.setExpiryDate(event.getExpiryDate());
|
||||
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<GetEventResponse> getEvent(UUID token) {
|
||||
Event event = getEventUseCase.getByEventToken(token)
|
||||
.orElseThrow(() -> new EventNotFoundException(token));
|
||||
|
||||
var response = new GetEventResponse();
|
||||
response.setEventToken(event.getEventToken());
|
||||
response.setTitle(event.getTitle());
|
||||
response.setDescription(event.getDescription());
|
||||
response.setDateTime(event.getDateTime());
|
||||
response.setTimezone(event.getTimezone().getId());
|
||||
response.setLocation(event.getLocation());
|
||||
response.setAttendeeCount(0);
|
||||
response.setExpired(
|
||||
event.getExpiryDate().isBefore(LocalDate.now(clock)));
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
private static ZoneId parseTimezone(String timezone) {
|
||||
try {
|
||||
return ZoneId.of(timezone);
|
||||
} catch (DateTimeException e) {
|
||||
throw new InvalidTimezoneException(timezone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package de.fete.adapter.in.web;
|
||||
|
||||
import de.fete.application.service.EventNotFoundException;
|
||||
import de.fete.application.service.ExpiryDateInPastException;
|
||||
import de.fete.application.service.InvalidTimezoneException;
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -57,6 +59,32 @@ public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
|
||||
.body(problemDetail);
|
||||
}
|
||||
|
||||
/** Handles event not found. */
|
||||
@ExceptionHandler(EventNotFoundException.class)
|
||||
public ResponseEntity<ProblemDetail> handleEventNotFound(
|
||||
EventNotFoundException ex) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.NOT_FOUND, ex.getMessage());
|
||||
problemDetail.setTitle("Event Not Found");
|
||||
problemDetail.setType(URI.create("urn:problem-type:event-not-found"));
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND)
|
||||
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||
.body(problemDetail);
|
||||
}
|
||||
|
||||
/** Handles invalid timezone. */
|
||||
@ExceptionHandler(InvalidTimezoneException.class)
|
||||
public ResponseEntity<ProblemDetail> handleInvalidTimezone(
|
||||
InvalidTimezoneException ex) {
|
||||
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(
|
||||
HttpStatus.BAD_REQUEST, ex.getMessage());
|
||||
problemDetail.setTitle("Invalid Timezone");
|
||||
problemDetail.setType(URI.create("urn:problem-type:invalid-timezone"));
|
||||
return ResponseEntity.badRequest()
|
||||
.contentType(MediaType.APPLICATION_PROBLEM_JSON)
|
||||
.body(problemDetail);
|
||||
}
|
||||
|
||||
/** Catches all unhandled exceptions. */
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ProblemDetail> handleAll(Exception ex) {
|
||||
|
||||
@@ -34,6 +34,9 @@ public class EventJpaEntity {
|
||||
@Column(name = "date_time", nullable = false)
|
||||
private OffsetDateTime dateTime;
|
||||
|
||||
@Column(nullable = false, length = 64)
|
||||
private String timezone;
|
||||
|
||||
@Column(length = 500)
|
||||
private String location;
|
||||
|
||||
@@ -103,6 +106,16 @@ public class EventJpaEntity {
|
||||
this.dateTime = dateTime;
|
||||
}
|
||||
|
||||
/** Returns the IANA timezone name. */
|
||||
public String getTimezone() {
|
||||
return timezone;
|
||||
}
|
||||
|
||||
/** Sets the IANA timezone name. */
|
||||
public void setTimezone(String timezone) {
|
||||
this.timezone = timezone;
|
||||
}
|
||||
|
||||
/** Returns the event location. */
|
||||
public String getLocation() {
|
||||
return location;
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.fete.adapter.out.persistence;
|
||||
|
||||
import de.fete.domain.model.Event;
|
||||
import de.fete.domain.port.out.EventRepository;
|
||||
import java.time.ZoneId;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@@ -37,6 +38,7 @@ public class EventPersistenceAdapter implements EventRepository {
|
||||
entity.setTitle(event.getTitle());
|
||||
entity.setDescription(event.getDescription());
|
||||
entity.setDateTime(event.getDateTime());
|
||||
entity.setTimezone(event.getTimezone().getId());
|
||||
entity.setLocation(event.getLocation());
|
||||
entity.setExpiryDate(event.getExpiryDate());
|
||||
entity.setCreatedAt(event.getCreatedAt());
|
||||
@@ -51,6 +53,7 @@ public class EventPersistenceAdapter implements EventRepository {
|
||||
event.setTitle(entity.getTitle());
|
||||
event.setDescription(entity.getDescription());
|
||||
event.setDateTime(entity.getDateTime());
|
||||
event.setTimezone(ZoneId.of(entity.getTimezone()));
|
||||
event.setLocation(entity.getLocation());
|
||||
event.setExpiryDate(entity.getExpiryDate());
|
||||
event.setCreatedAt(entity.getCreatedAt());
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.fete.application.service;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/** Thrown when an event cannot be found by its token. */
|
||||
public class EventNotFoundException extends RuntimeException {
|
||||
|
||||
/** Creates a new exception for the given event token. */
|
||||
public EventNotFoundException(UUID eventToken) {
|
||||
super("Event not found: " + eventToken);
|
||||
}
|
||||
}
|
||||
@@ -3,16 +3,18 @@ package de.fete.application.service;
|
||||
import de.fete.domain.model.CreateEventCommand;
|
||||
import de.fete.domain.model.Event;
|
||||
import de.fete.domain.port.in.CreateEventUseCase;
|
||||
import de.fete.domain.port.in.GetEventUseCase;
|
||||
import de.fete.domain.port.out.EventRepository;
|
||||
import java.time.Clock;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/** Application service implementing event creation. */
|
||||
/** Application service implementing event creation and retrieval. */
|
||||
@Service
|
||||
public class EventService implements CreateEventUseCase {
|
||||
public class EventService implements CreateEventUseCase, GetEventUseCase {
|
||||
|
||||
private final EventRepository eventRepository;
|
||||
private final Clock clock;
|
||||
@@ -35,10 +37,16 @@ public class EventService implements CreateEventUseCase {
|
||||
event.setTitle(command.title());
|
||||
event.setDescription(command.description());
|
||||
event.setDateTime(command.dateTime());
|
||||
event.setTimezone(command.timezone());
|
||||
event.setLocation(command.location());
|
||||
event.setExpiryDate(command.expiryDate());
|
||||
event.setCreatedAt(OffsetDateTime.now(clock));
|
||||
|
||||
return eventRepository.save(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Event> getByEventToken(UUID eventToken) {
|
||||
return eventRepository.findByEventToken(eventToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.fete.application.service;
|
||||
|
||||
/** Thrown when an invalid IANA timezone ID is provided. */
|
||||
public class InvalidTimezoneException extends RuntimeException {
|
||||
|
||||
/** Creates a new exception for the given invalid timezone. */
|
||||
public InvalidTimezoneException(String timezone) {
|
||||
super("Invalid IANA timezone: " + timezone);
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,14 @@ package de.fete.domain.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
|
||||
/** Command carrying the data needed to create an event. */
|
||||
public record CreateEventCommand(
|
||||
String title,
|
||||
String description,
|
||||
OffsetDateTime dateTime,
|
||||
ZoneId timezone,
|
||||
String location,
|
||||
LocalDate expiryDate
|
||||
) {}
|
||||
|
||||
@@ -2,6 +2,7 @@ package de.fete.domain.model;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Domain entity representing an event. */
|
||||
@@ -13,6 +14,7 @@ public class Event {
|
||||
private String title;
|
||||
private String description;
|
||||
private OffsetDateTime dateTime;
|
||||
private ZoneId timezone;
|
||||
private String location;
|
||||
private LocalDate expiryDate;
|
||||
private OffsetDateTime createdAt;
|
||||
@@ -77,6 +79,16 @@ public class Event {
|
||||
this.dateTime = dateTime;
|
||||
}
|
||||
|
||||
/** Returns the IANA timezone. */
|
||||
public ZoneId getTimezone() {
|
||||
return timezone;
|
||||
}
|
||||
|
||||
/** Sets the IANA timezone. */
|
||||
public void setTimezone(ZoneId timezone) {
|
||||
this.timezone = timezone;
|
||||
}
|
||||
|
||||
/** Returns the event location. */
|
||||
public String getLocation() {
|
||||
return location;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.fete.domain.port.in;
|
||||
|
||||
import de.fete.domain.model.Event;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Inbound port for retrieving a public event by its token. */
|
||||
public interface GetEventUseCase {
|
||||
|
||||
/** Finds an event by its public event token. */
|
||||
Optional<Event> getByEventToken(UUID eventToken);
|
||||
}
|
||||
Reference in New Issue
Block a user