Refactor domain models to records and move exceptions to sub-package
- Convert Event and Rsvp from mutable POJOs to Java records - Move all 8 exception classes to application.service.exception sub-package - Add ArchUnit rule enforcing domain models must be records Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,8 @@ import de.fete.adapter.in.web.model.CreateRsvpResponse;
|
|||||||
import de.fete.adapter.in.web.model.GetAttendeesResponse;
|
import de.fete.adapter.in.web.model.GetAttendeesResponse;
|
||||||
import de.fete.adapter.in.web.model.GetEventResponse;
|
import de.fete.adapter.in.web.model.GetEventResponse;
|
||||||
import de.fete.adapter.in.web.model.PatchEventRequest;
|
import de.fete.adapter.in.web.model.PatchEventRequest;
|
||||||
import de.fete.application.service.EventNotFoundException;
|
import de.fete.application.service.exception.EventNotFoundException;
|
||||||
import de.fete.application.service.InvalidTimezoneException;
|
import de.fete.application.service.exception.InvalidTimezoneException;
|
||||||
import de.fete.domain.model.CreateEventCommand;
|
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;
|
||||||
@@ -78,11 +78,11 @@ public class EventController implements EventsApi {
|
|||||||
Event event = createEventUseCase.createEvent(command);
|
Event event = createEventUseCase.createEvent(command);
|
||||||
|
|
||||||
var response = new CreateEventResponse();
|
var response = new CreateEventResponse();
|
||||||
response.setEventToken(event.getEventToken().value());
|
response.setEventToken(event.eventToken().value());
|
||||||
response.setOrganizerToken(event.getOrganizerToken().value());
|
response.setOrganizerToken(event.organizerToken().value());
|
||||||
response.setTitle(event.getTitle());
|
response.setTitle(event.title());
|
||||||
response.setDateTime(event.getDateTime());
|
response.setDateTime(event.dateTime());
|
||||||
response.setTimezone(event.getTimezone().getId());
|
response.setTimezone(event.timezone().getId());
|
||||||
|
|
||||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||||
}
|
}
|
||||||
@@ -94,16 +94,16 @@ public class EventController implements EventsApi {
|
|||||||
.orElseThrow(() -> new EventNotFoundException(eventToken));
|
.orElseThrow(() -> new EventNotFoundException(eventToken));
|
||||||
|
|
||||||
var response = new GetEventResponse();
|
var response = new GetEventResponse();
|
||||||
response.setEventToken(event.getEventToken().value());
|
response.setEventToken(event.eventToken().value());
|
||||||
response.setTitle(event.getTitle());
|
response.setTitle(event.title());
|
||||||
response.setDescription(event.getDescription());
|
response.setDescription(event.description());
|
||||||
response.setDateTime(event.getDateTime());
|
response.setDateTime(event.dateTime());
|
||||||
response.setTimezone(event.getTimezone().getId());
|
response.setTimezone(event.timezone().getId());
|
||||||
response.setLocation(event.getLocation());
|
response.setLocation(event.location());
|
||||||
response.setAttendeeCount(
|
response.setAttendeeCount(
|
||||||
(int) countAttendeesByEventUseCase.countByEvent(evtToken));
|
(int) countAttendeesByEventUseCase.countByEvent(evtToken));
|
||||||
response.setCancelled(event.isCancelled());
|
response.setCancelled(event.cancelled());
|
||||||
response.setCancellationReason(event.getCancellationReason());
|
response.setCancellationReason(event.cancellationReason());
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
@@ -145,8 +145,8 @@ public class EventController implements EventsApi {
|
|||||||
Rsvp rsvp = createRsvpUseCase.createRsvp(evtToken, createRsvpRequest.getName());
|
Rsvp rsvp = createRsvpUseCase.createRsvp(evtToken, createRsvpRequest.getName());
|
||||||
|
|
||||||
var response = new CreateRsvpResponse();
|
var response = new CreateRsvpResponse();
|
||||||
response.setRsvpToken(rsvp.getRsvpToken().value());
|
response.setRsvpToken(rsvp.rsvpToken().value());
|
||||||
response.setName(rsvp.getName());
|
response.setName(rsvp.name());
|
||||||
|
|
||||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package de.fete.adapter.in.web;
|
package de.fete.adapter.in.web;
|
||||||
|
|
||||||
import de.fete.application.service.EventAlreadyCancelledException;
|
import de.fete.application.service.exception.EventAlreadyCancelledException;
|
||||||
import de.fete.application.service.EventCancelledException;
|
import de.fete.application.service.exception.EventCancelledException;
|
||||||
import de.fete.application.service.EventExpiredException;
|
import de.fete.application.service.exception.EventExpiredException;
|
||||||
import de.fete.application.service.EventNotFoundException;
|
import de.fete.application.service.exception.EventNotFoundException;
|
||||||
import de.fete.application.service.ExpiryDateBeforeEventException;
|
import de.fete.application.service.exception.ExpiryDateBeforeEventException;
|
||||||
import de.fete.application.service.ExpiryDateInPastException;
|
import de.fete.application.service.exception.ExpiryDateInPastException;
|
||||||
import de.fete.application.service.InvalidOrganizerTokenException;
|
import de.fete.application.service.exception.InvalidOrganizerTokenException;
|
||||||
import de.fete.application.service.InvalidTimezoneException;
|
import de.fete.application.service.exception.InvalidTimezoneException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|||||||
@@ -86,11 +86,11 @@ public class SpaController {
|
|||||||
|
|
||||||
private Map<String, String> buildEventMeta(Event event, String baseUrl) {
|
private Map<String, String> buildEventMeta(Event event, String baseUrl) {
|
||||||
var tags = new LinkedHashMap<String, String>();
|
var tags = new LinkedHashMap<String, String>();
|
||||||
String title = truncateTitle(event.getTitle());
|
String title = truncateTitle(event.title());
|
||||||
String description = formatDescription(event);
|
String description = formatDescription(event);
|
||||||
tags.put("og:title", title);
|
tags.put("og:title", title);
|
||||||
tags.put("og:description", description);
|
tags.put("og:description", description);
|
||||||
tags.put("og:url", baseUrl + "/events/" + event.getEventToken().value());
|
tags.put("og:url", baseUrl + "/events/" + event.eventToken().value());
|
||||||
tags.put("og:type", "website");
|
tags.put("og:type", "website");
|
||||||
tags.put("og:site_name", GENERIC_TITLE);
|
tags.put("og:site_name", GENERIC_TITLE);
|
||||||
tags.put("og:image", baseUrl + "/og-image.png");
|
tags.put("og:image", baseUrl + "/og-image.png");
|
||||||
@@ -138,16 +138,16 @@ public class SpaController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String formatDescription(Event event) {
|
private String formatDescription(Event event) {
|
||||||
ZonedDateTime zoned = event.getDateTime().atZoneSameInstant(event.getTimezone());
|
ZonedDateTime zoned = event.dateTime().atZoneSameInstant(event.timezone());
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
sb.append("📅 ").append(zoned.format(DATE_FORMAT));
|
sb.append("📅 ").append(zoned.format(DATE_FORMAT));
|
||||||
|
|
||||||
if (event.getLocation() != null && !event.getLocation().isBlank()) {
|
if (event.location() != null && !event.location().isBlank()) {
|
||||||
sb.append(" · 📍 ").append(event.getLocation());
|
sb.append(" · 📍 ").append(event.location());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.getDescription() != null && !event.getDescription().isBlank()) {
|
if (event.description() != null && !event.description().isBlank()) {
|
||||||
sb.append(" — ").append(event.getDescription());
|
sb.append(" — ").append(event.description());
|
||||||
}
|
}
|
||||||
|
|
||||||
String result = sb.toString();
|
String result = sb.toString();
|
||||||
|
|||||||
@@ -38,35 +38,34 @@ public class EventPersistenceAdapter implements EventRepository {
|
|||||||
|
|
||||||
private EventJpaEntity toEntity(Event event) {
|
private EventJpaEntity toEntity(Event event) {
|
||||||
var entity = new EventJpaEntity();
|
var entity = new EventJpaEntity();
|
||||||
entity.setId(event.getId());
|
entity.setId(event.id());
|
||||||
entity.setEventToken(event.getEventToken().value());
|
entity.setEventToken(event.eventToken().value());
|
||||||
entity.setOrganizerToken(event.getOrganizerToken().value());
|
entity.setOrganizerToken(event.organizerToken().value());
|
||||||
entity.setTitle(event.getTitle());
|
entity.setTitle(event.title());
|
||||||
entity.setDescription(event.getDescription());
|
entity.setDescription(event.description());
|
||||||
entity.setDateTime(event.getDateTime());
|
entity.setDateTime(event.dateTime());
|
||||||
entity.setTimezone(event.getTimezone().getId());
|
entity.setTimezone(event.timezone().getId());
|
||||||
entity.setLocation(event.getLocation());
|
entity.setLocation(event.location());
|
||||||
entity.setExpiryDate(event.getExpiryDate());
|
entity.setExpiryDate(event.expiryDate());
|
||||||
entity.setCreatedAt(event.getCreatedAt());
|
entity.setCreatedAt(event.createdAt());
|
||||||
entity.setCancelled(event.isCancelled());
|
entity.setCancelled(event.cancelled());
|
||||||
entity.setCancellationReason(event.getCancellationReason());
|
entity.setCancellationReason(event.cancellationReason());
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Event toDomain(EventJpaEntity entity) {
|
private Event toDomain(EventJpaEntity entity) {
|
||||||
var event = new Event();
|
return new Event(
|
||||||
event.setId(entity.getId());
|
entity.getId(),
|
||||||
event.setEventToken(new EventToken(entity.getEventToken()));
|
new EventToken(entity.getEventToken()),
|
||||||
event.setOrganizerToken(new OrganizerToken(entity.getOrganizerToken()));
|
new OrganizerToken(entity.getOrganizerToken()),
|
||||||
event.setTitle(entity.getTitle());
|
entity.getTitle(),
|
||||||
event.setDescription(entity.getDescription());
|
entity.getDescription(),
|
||||||
event.setDateTime(entity.getDateTime());
|
entity.getDateTime(),
|
||||||
event.setTimezone(ZoneId.of(entity.getTimezone()));
|
ZoneId.of(entity.getTimezone()),
|
||||||
event.setLocation(entity.getLocation());
|
entity.getLocation(),
|
||||||
event.setExpiryDate(entity.getExpiryDate());
|
entity.getExpiryDate(),
|
||||||
event.setCreatedAt(entity.getCreatedAt());
|
entity.getCreatedAt(),
|
||||||
event.setCancelled(entity.isCancelled());
|
entity.isCancelled(),
|
||||||
event.setCancellationReason(entity.getCancellationReason());
|
entity.getCancellationReason());
|
||||||
return event;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,19 +43,18 @@ public class RsvpPersistenceAdapter implements RsvpRepository {
|
|||||||
|
|
||||||
private RsvpJpaEntity toEntity(Rsvp rsvp) {
|
private RsvpJpaEntity toEntity(Rsvp rsvp) {
|
||||||
var entity = new RsvpJpaEntity();
|
var entity = new RsvpJpaEntity();
|
||||||
entity.setId(rsvp.getId());
|
entity.setId(rsvp.id());
|
||||||
entity.setRsvpToken(rsvp.getRsvpToken().value());
|
entity.setRsvpToken(rsvp.rsvpToken().value());
|
||||||
entity.setEventId(rsvp.getEventId());
|
entity.setEventId(rsvp.eventId());
|
||||||
entity.setName(rsvp.getName());
|
entity.setName(rsvp.name());
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Rsvp toDomain(RsvpJpaEntity entity) {
|
private Rsvp toDomain(RsvpJpaEntity entity) {
|
||||||
var rsvp = new Rsvp();
|
return new Rsvp(
|
||||||
rsvp.setId(entity.getId());
|
entity.getId(),
|
||||||
rsvp.setRsvpToken(new RsvpToken(entity.getRsvpToken()));
|
new RsvpToken(entity.getRsvpToken()),
|
||||||
rsvp.setEventId(entity.getEventId());
|
entity.getEventId(),
|
||||||
rsvp.setName(entity.getName());
|
entity.getName());
|
||||||
return rsvp;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package de.fete.application.service;
|
package de.fete.application.service;
|
||||||
|
|
||||||
|
import de.fete.application.service.exception.EventAlreadyCancelledException;
|
||||||
|
import de.fete.application.service.exception.EventNotFoundException;
|
||||||
|
import de.fete.application.service.exception.InvalidOrganizerTokenException;
|
||||||
import de.fete.domain.model.CreateEventCommand;
|
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;
|
||||||
@@ -34,16 +37,19 @@ public class EventService implements CreateEventUseCase, GetEventUseCase, Update
|
|||||||
public Event createEvent(CreateEventCommand command) {
|
public Event createEvent(CreateEventCommand command) {
|
||||||
LocalDate expiryDate = command.dateTime().toLocalDate().plusDays(EXPIRY_DAYS_AFTER_EVENT);
|
LocalDate expiryDate = command.dateTime().toLocalDate().plusDays(EXPIRY_DAYS_AFTER_EVENT);
|
||||||
|
|
||||||
var event = new Event();
|
var event = new Event(
|
||||||
event.setEventToken(EventToken.generate());
|
null,
|
||||||
event.setOrganizerToken(OrganizerToken.generate());
|
EventToken.generate(),
|
||||||
event.setTitle(command.title());
|
OrganizerToken.generate(),
|
||||||
event.setDescription(command.description());
|
command.title(),
|
||||||
event.setDateTime(command.dateTime());
|
command.description(),
|
||||||
event.setTimezone(command.timezone());
|
command.dateTime(),
|
||||||
event.setLocation(command.location());
|
command.timezone(),
|
||||||
event.setExpiryDate(expiryDate);
|
command.location(),
|
||||||
event.setCreatedAt(OffsetDateTime.now(clock));
|
expiryDate,
|
||||||
|
OffsetDateTime.now(clock),
|
||||||
|
false,
|
||||||
|
null);
|
||||||
|
|
||||||
return eventRepository.save(event);
|
return eventRepository.save(event);
|
||||||
}
|
}
|
||||||
@@ -65,16 +71,14 @@ public class EventService implements CreateEventUseCase, GetEventUseCase, Update
|
|||||||
Event event = eventRepository.findByEventToken(eventToken)
|
Event event = eventRepository.findByEventToken(eventToken)
|
||||||
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
||||||
|
|
||||||
if (!event.getOrganizerToken().equals(organizerToken)) {
|
if (!event.organizerToken().equals(organizerToken)) {
|
||||||
throw new InvalidOrganizerTokenException();
|
throw new InvalidOrganizerTokenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.isCancelled()) {
|
if (event.cancelled()) {
|
||||||
throw new EventAlreadyCancelledException(eventToken.value());
|
throw new EventAlreadyCancelledException(eventToken.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
event.setCancelled(true);
|
eventRepository.save(event.withCancellation(true, reason));
|
||||||
event.setCancellationReason(reason);
|
|
||||||
eventRepository.save(event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
package de.fete.application.service;
|
package de.fete.application.service;
|
||||||
|
|
||||||
|
import de.fete.application.service.exception.EventCancelledException;
|
||||||
|
import de.fete.application.service.exception.EventExpiredException;
|
||||||
|
import de.fete.application.service.exception.EventNotFoundException;
|
||||||
|
import de.fete.application.service.exception.InvalidOrganizerTokenException;
|
||||||
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.OrganizerToken;
|
import de.fete.domain.model.OrganizerToken;
|
||||||
@@ -42,18 +46,15 @@ public class RsvpService
|
|||||||
Event event = eventRepository.findByEventToken(eventToken)
|
Event event = eventRepository.findByEventToken(eventToken)
|
||||||
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
||||||
|
|
||||||
if (event.isCancelled()) {
|
if (event.cancelled()) {
|
||||||
throw new EventCancelledException(eventToken.value());
|
throw new EventCancelledException(eventToken.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!event.getExpiryDate().isAfter(LocalDate.now(clock))) {
|
if (!event.expiryDate().isAfter(LocalDate.now(clock))) {
|
||||||
throw new EventExpiredException(eventToken.value());
|
throw new EventExpiredException(eventToken.value());
|
||||||
}
|
}
|
||||||
|
|
||||||
var rsvp = new Rsvp();
|
var rsvp = new Rsvp(null, RsvpToken.generate(), event.id(), name.strip());
|
||||||
rsvp.setRsvpToken(RsvpToken.generate());
|
|
||||||
rsvp.setEventId(event.getId());
|
|
||||||
rsvp.setName(name.strip());
|
|
||||||
|
|
||||||
return rsvpRepository.save(rsvp);
|
return rsvpRepository.save(rsvp);
|
||||||
}
|
}
|
||||||
@@ -63,14 +64,14 @@ public class RsvpService
|
|||||||
public void cancelRsvp(EventToken eventToken, RsvpToken rsvpToken) {
|
public void cancelRsvp(EventToken eventToken, RsvpToken rsvpToken) {
|
||||||
eventRepository.findByEventToken(eventToken)
|
eventRepository.findByEventToken(eventToken)
|
||||||
.ifPresent(event ->
|
.ifPresent(event ->
|
||||||
rsvpRepository.deleteByEventIdAndRsvpToken(event.getId(), rsvpToken));
|
rsvpRepository.deleteByEventIdAndRsvpToken(event.id(), rsvpToken));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long countByEvent(EventToken eventToken) {
|
public long countByEvent(EventToken eventToken) {
|
||||||
Event event = eventRepository.findByEventToken(eventToken)
|
Event event = eventRepository.findByEventToken(eventToken)
|
||||||
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
||||||
return rsvpRepository.countByEventId(event.getId());
|
return rsvpRepository.countByEventId(event.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -78,12 +79,12 @@ public class RsvpService
|
|||||||
Event event = eventRepository.findByEventToken(eventToken)
|
Event event = eventRepository.findByEventToken(eventToken)
|
||||||
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
.orElseThrow(() -> new EventNotFoundException(eventToken.value()));
|
||||||
|
|
||||||
if (!event.getOrganizerToken().equals(organizerToken)) {
|
if (!event.organizerToken().equals(organizerToken)) {
|
||||||
throw new InvalidOrganizerTokenException();
|
throw new InvalidOrganizerTokenException();
|
||||||
}
|
}
|
||||||
|
|
||||||
return rsvpRepository.findByEventId(event.getId()).stream()
|
return rsvpRepository.findByEventId(event.id()).stream()
|
||||||
.map(Rsvp::getName)
|
.map(Rsvp::name)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package de.fete.application.service;
|
package de.fete.application.service.exception;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package de.fete.application.service;
|
package de.fete.application.service.exception;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package de.fete.application.service;
|
package de.fete.application.service.exception;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package de.fete.application.service;
|
package de.fete.application.service.exception;
|
||||||
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package de.fete.application.service;
|
package de.fete.application.service.exception;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package de.fete.application.service;
|
package de.fete.application.service.exception;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package de.fete.application.service;
|
package de.fete.application.service.exception;
|
||||||
|
|
||||||
/** Thrown when an invalid organizer token is provided. */
|
/** Thrown when an invalid organizer token is provided. */
|
||||||
public class InvalidOrganizerTokenException extends RuntimeException {
|
public class InvalidOrganizerTokenException extends RuntimeException {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package de.fete.application.service;
|
package de.fete.application.service.exception;
|
||||||
|
|
||||||
/** Thrown when an invalid IANA timezone ID is provided. */
|
/** Thrown when an invalid IANA timezone ID is provided. */
|
||||||
public class InvalidTimezoneException extends RuntimeException {
|
public class InvalidTimezoneException extends RuntimeException {
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* Application-layer exceptions thrown by service use case implementations.
|
||||||
|
*/
|
||||||
|
package de.fete.application.service.exception;
|
||||||
@@ -5,138 +5,26 @@ import java.time.OffsetDateTime;
|
|||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
|
|
||||||
/** Domain entity representing an event. */
|
/** Domain entity representing an event. */
|
||||||
public class Event {
|
public record Event(
|
||||||
|
Long id,
|
||||||
|
EventToken eventToken,
|
||||||
|
OrganizerToken organizerToken,
|
||||||
|
String title,
|
||||||
|
String description,
|
||||||
|
OffsetDateTime dateTime,
|
||||||
|
ZoneId timezone,
|
||||||
|
String location,
|
||||||
|
LocalDate expiryDate,
|
||||||
|
OffsetDateTime createdAt,
|
||||||
|
boolean cancelled,
|
||||||
|
String cancellationReason
|
||||||
|
) {
|
||||||
|
|
||||||
private Long id;
|
/** Returns a copy of this event with cancellation applied. */
|
||||||
private EventToken eventToken;
|
public Event withCancellation(boolean cancelled, String cancellationReason) {
|
||||||
private OrganizerToken organizerToken;
|
return new Event(
|
||||||
private String title;
|
id, eventToken, organizerToken, title, description,
|
||||||
private String description;
|
dateTime, timezone, location, expiryDate, createdAt,
|
||||||
private OffsetDateTime dateTime;
|
cancelled, cancellationReason);
|
||||||
private ZoneId timezone;
|
|
||||||
private String location;
|
|
||||||
private LocalDate expiryDate;
|
|
||||||
private OffsetDateTime createdAt;
|
|
||||||
private boolean cancelled;
|
|
||||||
private String cancellationReason;
|
|
||||||
|
|
||||||
/** Returns the internal database ID. */
|
|
||||||
public Long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the internal database ID. */
|
|
||||||
public void setId(Long id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the public event token. */
|
|
||||||
public EventToken getEventToken() {
|
|
||||||
return eventToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the public event token. */
|
|
||||||
public void setEventToken(EventToken eventToken) {
|
|
||||||
this.eventToken = eventToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the secret organizer token. */
|
|
||||||
public OrganizerToken getOrganizerToken() {
|
|
||||||
return organizerToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the secret organizer token. */
|
|
||||||
public void setOrganizerToken(OrganizerToken organizerToken) {
|
|
||||||
this.organizerToken = organizerToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the event title. */
|
|
||||||
public String getTitle() {
|
|
||||||
return title;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the event title. */
|
|
||||||
public void setTitle(String title) {
|
|
||||||
this.title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the event description. */
|
|
||||||
public String getDescription() {
|
|
||||||
return description;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the event description. */
|
|
||||||
public void setDescription(String description) {
|
|
||||||
this.description = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the event date and time with UTC offset. */
|
|
||||||
public OffsetDateTime getDateTime() {
|
|
||||||
return dateTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the event date and time. */
|
|
||||||
public void setDateTime(OffsetDateTime dateTime) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the event location. */
|
|
||||||
public void setLocation(String location) {
|
|
||||||
this.location = location;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the expiry date after which event data is deleted. */
|
|
||||||
public LocalDate getExpiryDate() {
|
|
||||||
return expiryDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the expiry date. */
|
|
||||||
public void setExpiryDate(LocalDate expiryDate) {
|
|
||||||
this.expiryDate = expiryDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the creation timestamp. */
|
|
||||||
public OffsetDateTime getCreatedAt() {
|
|
||||||
return createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the creation timestamp. */
|
|
||||||
public void setCreatedAt(OffsetDateTime createdAt) {
|
|
||||||
this.createdAt = createdAt;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns whether the event has been cancelled. */
|
|
||||||
public boolean isCancelled() {
|
|
||||||
return cancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the cancelled flag. */
|
|
||||||
public void setCancelled(boolean cancelled) {
|
|
||||||
this.cancelled = cancelled;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the cancellation reason, if any. */
|
|
||||||
public String getCancellationReason() {
|
|
||||||
return cancellationReason;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the cancellation reason. */
|
|
||||||
public void setCancellationReason(String cancellationReason) {
|
|
||||||
this.cancellationReason = cancellationReason;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,50 +1,9 @@
|
|||||||
package de.fete.domain.model;
|
package de.fete.domain.model;
|
||||||
|
|
||||||
/** Domain entity representing an RSVP. */
|
/** Domain entity representing an RSVP. */
|
||||||
public class Rsvp {
|
public record Rsvp(
|
||||||
|
Long id,
|
||||||
private Long id;
|
RsvpToken rsvpToken,
|
||||||
private RsvpToken rsvpToken;
|
Long eventId,
|
||||||
private Long eventId;
|
String name
|
||||||
private String name;
|
) {}
|
||||||
|
|
||||||
/** Returns the internal database ID. */
|
|
||||||
public Long getId() {
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the internal database ID. */
|
|
||||||
public void setId(Long id) {
|
|
||||||
this.id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the RSVP token. */
|
|
||||||
public RsvpToken getRsvpToken() {
|
|
||||||
return rsvpToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the RSVP token. */
|
|
||||||
public void setRsvpToken(RsvpToken rsvpToken) {
|
|
||||||
this.rsvpToken = rsvpToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the event ID this RSVP belongs to. */
|
|
||||||
public Long getEventId() {
|
|
||||||
return eventId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the event ID. */
|
|
||||||
public void setEventId(Long eventId) {
|
|
||||||
this.eventId = eventId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the guest's display name. */
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sets the guest's display name. */
|
|
||||||
public void setName(String name) {
|
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,10 +4,14 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
|
|||||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||||
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
|
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
|
||||||
|
|
||||||
|
import com.tngtech.archunit.core.domain.JavaClass;
|
||||||
import com.tngtech.archunit.core.importer.ImportOption;
|
import com.tngtech.archunit.core.importer.ImportOption;
|
||||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||||
import com.tngtech.archunit.junit.ArchTest;
|
import com.tngtech.archunit.junit.ArchTest;
|
||||||
|
import com.tngtech.archunit.lang.ArchCondition;
|
||||||
import com.tngtech.archunit.lang.ArchRule;
|
import com.tngtech.archunit.lang.ArchRule;
|
||||||
|
import com.tngtech.archunit.lang.ConditionEvents;
|
||||||
|
import com.tngtech.archunit.lang.SimpleConditionEvent;
|
||||||
|
|
||||||
@AnalyzeClasses(packages = "de.fete", importOptions = ImportOption.DoNotIncludeTests.class)
|
@AnalyzeClasses(packages = "de.fete", importOptions = ImportOption.DoNotIncludeTests.class)
|
||||||
class HexagonalArchitectureTest {
|
class HexagonalArchitectureTest {
|
||||||
@@ -65,4 +69,24 @@ class HexagonalArchitectureTest {
|
|||||||
static final ArchRule webAdapterMustNotDependOnOutboundPorts = noClasses()
|
static final ArchRule webAdapterMustNotDependOnOutboundPorts = noClasses()
|
||||||
.that().resideInAPackage("de.fete.adapter.in.web..")
|
.that().resideInAPackage("de.fete.adapter.in.web..")
|
||||||
.should().dependOnClassesThat().resideInAPackage("de.fete.domain.port.out..");
|
.should().dependOnClassesThat().resideInAPackage("de.fete.domain.port.out..");
|
||||||
|
|
||||||
|
@ArchTest
|
||||||
|
static final ArchRule domainModelsMustBeRecords = classes()
|
||||||
|
.that().resideInAPackage("de.fete.domain.model..")
|
||||||
|
.and().doNotHaveSimpleName("package-info")
|
||||||
|
.should(beRecords());
|
||||||
|
|
||||||
|
private static ArchCondition<JavaClass> beRecords() {
|
||||||
|
return new ArchCondition<>("be records") {
|
||||||
|
@Override
|
||||||
|
public void check(JavaClass javaClass,
|
||||||
|
ConditionEvents events) {
|
||||||
|
boolean isRecord = javaClass.reflect().isRecord();
|
||||||
|
if (!isRecord) {
|
||||||
|
events.add(SimpleConditionEvent.violated(javaClass,
|
||||||
|
javaClass.getFullName() + " is not a record"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class EventPersistenceAdapterIntegrationTest {
|
|||||||
|
|
||||||
eventRepository.deleteExpired();
|
eventRepository.deleteExpired();
|
||||||
|
|
||||||
assertThat(eventRepository.findByEventToken(saved.getEventToken())).isPresent();
|
assertThat(eventRepository.findByEventToken(saved.eventToken())).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -52,7 +52,7 @@ class EventPersistenceAdapterIntegrationTest {
|
|||||||
|
|
||||||
eventRepository.deleteExpired();
|
eventRepository.deleteExpired();
|
||||||
|
|
||||||
assertThat(eventRepository.findByEventToken(saved.getEventToken())).isPresent();
|
assertThat(eventRepository.findByEventToken(saved.eventToken())).isPresent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -66,16 +66,18 @@ class EventPersistenceAdapterIntegrationTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Event buildEvent(String title, LocalDate expiryDate) {
|
private Event buildEvent(String title, LocalDate expiryDate) {
|
||||||
var event = new Event();
|
return new Event(
|
||||||
event.setEventToken(EventToken.generate());
|
null,
|
||||||
event.setOrganizerToken(OrganizerToken.generate());
|
EventToken.generate(),
|
||||||
event.setTitle(title);
|
OrganizerToken.generate(),
|
||||||
event.setDescription("Test description");
|
title,
|
||||||
event.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)));
|
"Test description",
|
||||||
event.setTimezone(ZoneId.of("Europe/Berlin"));
|
OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)),
|
||||||
event.setLocation("Test Location");
|
ZoneId.of("Europe/Berlin"),
|
||||||
event.setExpiryDate(expiryDate);
|
"Test Location",
|
||||||
event.setCreatedAt(OffsetDateTime.now());
|
expiryDate,
|
||||||
return event;
|
OffsetDateTime.now(),
|
||||||
|
false,
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ class EventPersistenceAdapterTest {
|
|||||||
|
|
||||||
Event saved = eventRepository.save(event);
|
Event saved = eventRepository.save(event);
|
||||||
|
|
||||||
assertThat(saved.getId()).isNotNull();
|
assertThat(saved.id()).isNotNull();
|
||||||
assertThat(saved.getTitle()).isEqualTo("Test Event");
|
assertThat(saved.title()).isEqualTo("Test Event");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -39,11 +39,11 @@ class EventPersistenceAdapterTest {
|
|||||||
Event event = buildEvent();
|
Event event = buildEvent();
|
||||||
Event saved = eventRepository.save(event);
|
Event saved = eventRepository.save(event);
|
||||||
|
|
||||||
Optional<Event> found = eventRepository.findByEventToken(saved.getEventToken());
|
Optional<Event> found = eventRepository.findByEventToken(saved.eventToken());
|
||||||
|
|
||||||
assertThat(found).isPresent();
|
assertThat(found).isPresent();
|
||||||
assertThat(found.get().getTitle()).isEqualTo("Test Event");
|
assertThat(found.get().title()).isEqualTo("Test Event");
|
||||||
assertThat(found.get().getId()).isEqualTo(saved.getId());
|
assertThat(found.get().id()).isEqualTo(saved.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -61,42 +61,47 @@ class EventPersistenceAdapterTest {
|
|||||||
OffsetDateTime createdAt =
|
OffsetDateTime createdAt =
|
||||||
OffsetDateTime.of(2026, 3, 4, 12, 0, 0, 0, ZoneOffset.UTC);
|
OffsetDateTime.of(2026, 3, 4, 12, 0, 0, 0, ZoneOffset.UTC);
|
||||||
|
|
||||||
var event = new Event();
|
var event = new Event(
|
||||||
event.setEventToken(EventToken.generate());
|
null,
|
||||||
event.setOrganizerToken(OrganizerToken.generate());
|
EventToken.generate(),
|
||||||
event.setTitle("Full Event");
|
OrganizerToken.generate(),
|
||||||
event.setDescription("A detailed description");
|
"Full Event",
|
||||||
event.setDateTime(dateTime);
|
"A detailed description",
|
||||||
event.setTimezone(ZoneId.of("Europe/Berlin"));
|
dateTime,
|
||||||
event.setLocation("Berlin, Germany");
|
ZoneId.of("Europe/Berlin"),
|
||||||
event.setExpiryDate(expiryDate);
|
"Berlin, Germany",
|
||||||
event.setCreatedAt(createdAt);
|
expiryDate,
|
||||||
|
createdAt,
|
||||||
|
false,
|
||||||
|
null);
|
||||||
|
|
||||||
Event saved = eventRepository.save(event);
|
Event saved = eventRepository.save(event);
|
||||||
Event found = eventRepository.findByEventToken(saved.getEventToken()).orElseThrow();
|
Event found = eventRepository.findByEventToken(saved.eventToken()).orElseThrow();
|
||||||
|
|
||||||
assertThat(found.getEventToken()).isEqualTo(event.getEventToken());
|
assertThat(found.eventToken()).isEqualTo(event.eventToken());
|
||||||
assertThat(found.getOrganizerToken()).isEqualTo(event.getOrganizerToken());
|
assertThat(found.organizerToken()).isEqualTo(event.organizerToken());
|
||||||
assertThat(found.getTitle()).isEqualTo("Full Event");
|
assertThat(found.title()).isEqualTo("Full Event");
|
||||||
assertThat(found.getDescription()).isEqualTo("A detailed description");
|
assertThat(found.description()).isEqualTo("A detailed description");
|
||||||
assertThat(found.getDateTime().toInstant()).isEqualTo(dateTime.toInstant());
|
assertThat(found.dateTime().toInstant()).isEqualTo(dateTime.toInstant());
|
||||||
assertThat(found.getTimezone()).isEqualTo(ZoneId.of("Europe/Berlin"));
|
assertThat(found.timezone()).isEqualTo(ZoneId.of("Europe/Berlin"));
|
||||||
assertThat(found.getLocation()).isEqualTo("Berlin, Germany");
|
assertThat(found.location()).isEqualTo("Berlin, Germany");
|
||||||
assertThat(found.getExpiryDate()).isEqualTo(expiryDate);
|
assertThat(found.expiryDate()).isEqualTo(expiryDate);
|
||||||
assertThat(found.getCreatedAt().toInstant()).isEqualTo(createdAt.toInstant());
|
assertThat(found.createdAt().toInstant()).isEqualTo(createdAt.toInstant());
|
||||||
}
|
}
|
||||||
|
|
||||||
private Event buildEvent() {
|
private Event buildEvent() {
|
||||||
var event = new Event();
|
return new Event(
|
||||||
event.setEventToken(EventToken.generate());
|
null,
|
||||||
event.setOrganizerToken(OrganizerToken.generate());
|
EventToken.generate(),
|
||||||
event.setTitle("Test Event");
|
OrganizerToken.generate(),
|
||||||
event.setDescription("Test description");
|
"Test Event",
|
||||||
event.setDateTime(OffsetDateTime.now().plusDays(7));
|
"Test description",
|
||||||
event.setTimezone(ZoneId.of("Europe/Berlin"));
|
OffsetDateTime.now().plusDays(7),
|
||||||
event.setLocation("Somewhere");
|
ZoneId.of("Europe/Berlin"),
|
||||||
event.setExpiryDate(LocalDate.now().plusDays(30));
|
"Somewhere",
|
||||||
event.setCreatedAt(OffsetDateTime.now());
|
LocalDate.now().plusDays(30),
|
||||||
return event;
|
OffsetDateTime.now(),
|
||||||
|
false,
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import static org.mockito.Mockito.never;
|
|||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import de.fete.application.service.EventAlreadyCancelledException;
|
import de.fete.application.service.exception.EventAlreadyCancelledException;
|
||||||
|
import de.fete.application.service.exception.EventNotFoundException;
|
||||||
|
import de.fete.application.service.exception.InvalidOrganizerTokenException;
|
||||||
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.OrganizerToken;
|
import de.fete.domain.model.OrganizerToken;
|
||||||
@@ -46,10 +48,8 @@ class EventServiceCancelTest {
|
|||||||
void cancelEventDelegatesToDomainAndSaves() {
|
void cancelEventDelegatesToDomainAndSaves() {
|
||||||
EventToken eventToken = EventToken.generate();
|
EventToken eventToken = EventToken.generate();
|
||||||
OrganizerToken organizerToken = OrganizerToken.generate();
|
OrganizerToken organizerToken = OrganizerToken.generate();
|
||||||
var event = new Event();
|
var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
|
||||||
event.setEventToken(eventToken);
|
null, false, null);
|
||||||
event.setOrganizerToken(organizerToken);
|
|
||||||
event.setCancelled(false);
|
|
||||||
|
|
||||||
when(eventRepository.findByEventToken(eventToken))
|
when(eventRepository.findByEventToken(eventToken))
|
||||||
.thenReturn(Optional.of(event));
|
.thenReturn(Optional.of(event));
|
||||||
@@ -60,18 +60,16 @@ class EventServiceCancelTest {
|
|||||||
|
|
||||||
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
|
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
|
||||||
verify(eventRepository).save(captor.capture());
|
verify(eventRepository).save(captor.capture());
|
||||||
assertThat(captor.getValue().isCancelled()).isTrue();
|
assertThat(captor.getValue().cancelled()).isTrue();
|
||||||
assertThat(captor.getValue().getCancellationReason()).isEqualTo("Venue unavailable");
|
assertThat(captor.getValue().cancellationReason()).isEqualTo("Venue unavailable");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void cancelEventWithNullReason() {
|
void cancelEventWithNullReason() {
|
||||||
EventToken eventToken = EventToken.generate();
|
EventToken eventToken = EventToken.generate();
|
||||||
OrganizerToken organizerToken = OrganizerToken.generate();
|
OrganizerToken organizerToken = OrganizerToken.generate();
|
||||||
var event = new Event();
|
var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
|
||||||
event.setEventToken(eventToken);
|
null, false, null);
|
||||||
event.setOrganizerToken(organizerToken);
|
|
||||||
event.setCancelled(false);
|
|
||||||
|
|
||||||
when(eventRepository.findByEventToken(eventToken))
|
when(eventRepository.findByEventToken(eventToken))
|
||||||
.thenReturn(Optional.of(event));
|
.thenReturn(Optional.of(event));
|
||||||
@@ -82,8 +80,8 @@ class EventServiceCancelTest {
|
|||||||
|
|
||||||
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
|
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
|
||||||
verify(eventRepository).save(captor.capture());
|
verify(eventRepository).save(captor.capture());
|
||||||
assertThat(captor.getValue().isCancelled()).isTrue();
|
assertThat(captor.getValue().cancelled()).isTrue();
|
||||||
assertThat(captor.getValue().getCancellationReason()).isNull();
|
assertThat(captor.getValue().cancellationReason()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -104,9 +102,8 @@ class EventServiceCancelTest {
|
|||||||
void cancelEventThrows403WhenWrongOrganizerToken() {
|
void cancelEventThrows403WhenWrongOrganizerToken() {
|
||||||
EventToken eventToken = EventToken.generate();
|
EventToken eventToken = EventToken.generate();
|
||||||
OrganizerToken correctToken = OrganizerToken.generate();
|
OrganizerToken correctToken = OrganizerToken.generate();
|
||||||
var event = new Event();
|
var event = new Event(null, eventToken, correctToken, null, null, null, null, null, null,
|
||||||
event.setEventToken(eventToken);
|
null, false, null);
|
||||||
event.setOrganizerToken(correctToken);
|
|
||||||
|
|
||||||
when(eventRepository.findByEventToken(eventToken))
|
when(eventRepository.findByEventToken(eventToken))
|
||||||
.thenReturn(Optional.of(event));
|
.thenReturn(Optional.of(event));
|
||||||
@@ -122,10 +119,8 @@ class EventServiceCancelTest {
|
|||||||
void cancelEventThrows409WhenAlreadyCancelled() {
|
void cancelEventThrows409WhenAlreadyCancelled() {
|
||||||
EventToken eventToken = EventToken.generate();
|
EventToken eventToken = EventToken.generate();
|
||||||
OrganizerToken organizerToken = OrganizerToken.generate();
|
OrganizerToken organizerToken = OrganizerToken.generate();
|
||||||
var event = new Event();
|
var event = new Event(null, eventToken, organizerToken, null, null, null, null, null, null,
|
||||||
event.setEventToken(eventToken);
|
null, true, null);
|
||||||
event.setOrganizerToken(organizerToken);
|
|
||||||
event.setCancelled(true);
|
|
||||||
|
|
||||||
when(eventRepository.findByEventToken(eventToken))
|
when(eventRepository.findByEventToken(eventToken))
|
||||||
.thenReturn(Optional.of(event));
|
.thenReturn(Optional.of(event));
|
||||||
|
|||||||
@@ -57,13 +57,13 @@ class EventServiceTest {
|
|||||||
|
|
||||||
Event result = eventService.createEvent(command);
|
Event result = eventService.createEvent(command);
|
||||||
|
|
||||||
assertThat(result.getTitle()).isEqualTo("Birthday Party");
|
assertThat(result.title()).isEqualTo("Birthday Party");
|
||||||
assertThat(result.getDescription()).isEqualTo("Come celebrate!");
|
assertThat(result.description()).isEqualTo("Come celebrate!");
|
||||||
assertThat(result.getTimezone()).isEqualTo(ZONE);
|
assertThat(result.timezone()).isEqualTo(ZONE);
|
||||||
assertThat(result.getLocation()).isEqualTo("Berlin");
|
assertThat(result.location()).isEqualTo("Berlin");
|
||||||
assertThat(result.getEventToken()).isNotNull();
|
assertThat(result.eventToken()).isNotNull();
|
||||||
assertThat(result.getOrganizerToken()).isNotNull();
|
assertThat(result.organizerToken()).isNotNull();
|
||||||
assertThat(result.getCreatedAt()).isEqualTo(OffsetDateTime.ofInstant(FIXED_INSTANT, ZONE));
|
assertThat(result.createdAt()).isEqualTo(OffsetDateTime.ofInstant(FIXED_INSTANT, ZONE));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -80,7 +80,7 @@ class EventServiceTest {
|
|||||||
|
|
||||||
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
|
ArgumentCaptor<Event> captor = ArgumentCaptor.forClass(Event.class);
|
||||||
verify(eventRepository, times(1)).save(captor.capture());
|
verify(eventRepository, times(1)).save(captor.capture());
|
||||||
assertThat(captor.getValue().getTitle()).isEqualTo("Test");
|
assertThat(captor.getValue().title()).isEqualTo("Test");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -96,7 +96,7 @@ class EventServiceTest {
|
|||||||
|
|
||||||
Event result = eventService.createEvent(command);
|
Event result = eventService.createEvent(command);
|
||||||
|
|
||||||
assertThat(result.getExpiryDate()).isEqualTo(eventDate.plusDays(7));
|
assertThat(result.expiryDate()).isEqualTo(eventDate.plusDays(7));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- GetEventUseCase tests (T004) ---
|
// --- GetEventUseCase tests (T004) ---
|
||||||
@@ -104,16 +104,15 @@ class EventServiceTest {
|
|||||||
@Test
|
@Test
|
||||||
void getByEventTokenReturnsEvent() {
|
void getByEventTokenReturnsEvent() {
|
||||||
EventToken token = EventToken.generate();
|
EventToken token = EventToken.generate();
|
||||||
var event = new Event();
|
var event = new Event(null, token, null, "Found Event", null, null, null, null, null, null,
|
||||||
event.setEventToken(token);
|
false, null);
|
||||||
event.setTitle("Found Event");
|
|
||||||
when(eventRepository.findByEventToken(token))
|
when(eventRepository.findByEventToken(token))
|
||||||
.thenReturn(Optional.of(event));
|
.thenReturn(Optional.of(event));
|
||||||
|
|
||||||
Optional<Event> result = eventService.getByEventToken(token);
|
Optional<Event> result = eventService.getByEventToken(token);
|
||||||
|
|
||||||
assertThat(result).isPresent();
|
assertThat(result).isPresent();
|
||||||
assertThat(result.get().getTitle()).isEqualTo("Found Event");
|
assertThat(result.get().title()).isEqualTo("Found Event");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -142,6 +141,6 @@ class EventServiceTest {
|
|||||||
|
|
||||||
Event result = eventService.createEvent(command);
|
Event result = eventService.createEvent(command);
|
||||||
|
|
||||||
assertThat(result.getTimezone()).isEqualTo(ZoneId.of("America/New_York"));
|
assertThat(result.timezone()).isEqualTo(ZoneId.of("America/New_York"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import static org.mockito.ArgumentMatchers.any;
|
|||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
import de.fete.application.service.exception.EventCancelledException;
|
||||||
|
import de.fete.application.service.exception.EventExpiredException;
|
||||||
|
import de.fete.application.service.exception.EventNotFoundException;
|
||||||
|
import de.fete.application.service.exception.InvalidOrganizerTokenException;
|
||||||
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.OrganizerToken;
|
import de.fete.domain.model.OrganizerToken;
|
||||||
@@ -51,23 +55,23 @@ class RsvpServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createRsvpSucceedsForActiveEvent() {
|
void createRsvpSucceedsForActiveEvent() {
|
||||||
Event event = buildActiveEvent();
|
Event event = buildActiveEvent(TODAY.plusDays(30));
|
||||||
EventToken token = event.getEventToken();
|
EventToken token = event.eventToken();
|
||||||
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
||||||
when(rsvpRepository.save(any(Rsvp.class)))
|
when(rsvpRepository.save(any(Rsvp.class)))
|
||||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
Rsvp result = rsvpService.createRsvp(token, "Max Mustermann");
|
Rsvp result = rsvpService.createRsvp(token, "Max Mustermann");
|
||||||
|
|
||||||
assertThat(result.getName()).isEqualTo("Max Mustermann");
|
assertThat(result.name()).isEqualTo("Max Mustermann");
|
||||||
assertThat(result.getRsvpToken()).isNotNull();
|
assertThat(result.rsvpToken()).isNotNull();
|
||||||
assertThat(result.getEventId()).isEqualTo(event.getId());
|
assertThat(result.eventId()).isEqualTo(event.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createRsvpPersistsViaRepository() {
|
void createRsvpPersistsViaRepository() {
|
||||||
Event event = buildActiveEvent();
|
Event event = buildActiveEvent(TODAY.plusDays(30));
|
||||||
EventToken token = event.getEventToken();
|
EventToken token = event.eventToken();
|
||||||
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
||||||
when(rsvpRepository.save(any(Rsvp.class)))
|
when(rsvpRepository.save(any(Rsvp.class)))
|
||||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
@@ -76,8 +80,8 @@ class RsvpServiceTest {
|
|||||||
|
|
||||||
ArgumentCaptor<Rsvp> captor = ArgumentCaptor.forClass(Rsvp.class);
|
ArgumentCaptor<Rsvp> captor = ArgumentCaptor.forClass(Rsvp.class);
|
||||||
verify(rsvpRepository).save(captor.capture());
|
verify(rsvpRepository).save(captor.capture());
|
||||||
assertThat(captor.getValue().getName()).isEqualTo("Test Guest");
|
assertThat(captor.getValue().name()).isEqualTo("Test Guest");
|
||||||
assertThat(captor.getValue().getEventId()).isEqualTo(event.getId());
|
assertThat(captor.getValue().eventId()).isEqualTo(event.id());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -91,22 +95,21 @@ class RsvpServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createRsvpTrimsName() {
|
void createRsvpTrimsName() {
|
||||||
Event event = buildActiveEvent();
|
Event event = buildActiveEvent(TODAY.plusDays(30));
|
||||||
EventToken token = event.getEventToken();
|
EventToken token = event.eventToken();
|
||||||
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
||||||
when(rsvpRepository.save(any(Rsvp.class)))
|
when(rsvpRepository.save(any(Rsvp.class)))
|
||||||
.thenAnswer(invocation -> invocation.getArgument(0));
|
.thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
Rsvp result = rsvpService.createRsvp(token, " Max ");
|
Rsvp result = rsvpService.createRsvp(token, " Max ");
|
||||||
|
|
||||||
assertThat(result.getName()).isEqualTo("Max");
|
assertThat(result.name()).isEqualTo("Max");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createRsvpThrowsWhenEventExpired() {
|
void createRsvpThrowsWhenEventExpired() {
|
||||||
var event = buildActiveEvent();
|
Event event = buildActiveEvent(TODAY.minusDays(1));
|
||||||
event.setExpiryDate(TODAY.minusDays(1));
|
EventToken token = event.eventToken();
|
||||||
EventToken token = event.getEventToken();
|
|
||||||
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
||||||
|
|
||||||
assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
|
assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
|
||||||
@@ -115,9 +118,8 @@ class RsvpServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void createRsvpThrowsWhenEventExpiresToday() {
|
void createRsvpThrowsWhenEventExpiresToday() {
|
||||||
var event = buildActiveEvent();
|
Event event = buildActiveEvent(TODAY);
|
||||||
event.setExpiryDate(TODAY);
|
EventToken token = event.eventToken();
|
||||||
EventToken token = event.getEventToken();
|
|
||||||
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
||||||
|
|
||||||
assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
|
assertThatThrownBy(() -> rsvpService.createRsvp(token, "Late Guest"))
|
||||||
@@ -126,12 +128,12 @@ class RsvpServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getAttendeeNamesReturnsNamesInOrder() {
|
void getAttendeeNamesReturnsNamesInOrder() {
|
||||||
Event event = buildActiveEvent();
|
Event event = buildActiveEvent(TODAY.plusDays(30));
|
||||||
EventToken token = event.getEventToken();
|
EventToken token = event.eventToken();
|
||||||
OrganizerToken orgToken = event.getOrganizerToken();
|
OrganizerToken orgToken = event.organizerToken();
|
||||||
when(eventRepository.findByEventToken(token))
|
when(eventRepository.findByEventToken(token))
|
||||||
.thenReturn(Optional.of(event));
|
.thenReturn(Optional.of(event));
|
||||||
when(rsvpRepository.findByEventId(event.getId()))
|
when(rsvpRepository.findByEventId(event.id()))
|
||||||
.thenReturn(List.of(
|
.thenReturn(List.of(
|
||||||
buildRsvp(1L, "Alice"),
|
buildRsvp(1L, "Alice"),
|
||||||
buildRsvp(2L, "Bob"),
|
buildRsvp(2L, "Bob"),
|
||||||
@@ -144,12 +146,12 @@ class RsvpServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getAttendeeNamesReturnsEmptyListWhenNoRsvps() {
|
void getAttendeeNamesReturnsEmptyListWhenNoRsvps() {
|
||||||
Event event = buildActiveEvent();
|
Event event = buildActiveEvent(TODAY.plusDays(30));
|
||||||
EventToken token = event.getEventToken();
|
EventToken token = event.eventToken();
|
||||||
OrganizerToken orgToken = event.getOrganizerToken();
|
OrganizerToken orgToken = event.organizerToken();
|
||||||
when(eventRepository.findByEventToken(token))
|
when(eventRepository.findByEventToken(token))
|
||||||
.thenReturn(Optional.of(event));
|
.thenReturn(Optional.of(event));
|
||||||
when(rsvpRepository.findByEventId(event.getId()))
|
when(rsvpRepository.findByEventId(event.id()))
|
||||||
.thenReturn(List.of());
|
.thenReturn(List.of());
|
||||||
|
|
||||||
List<String> names = rsvpService.getAttendeeNames(token, orgToken);
|
List<String> names = rsvpService.getAttendeeNames(token, orgToken);
|
||||||
@@ -171,8 +173,8 @@ class RsvpServiceTest {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
void getAttendeeNamesThrowsWhenOrganizerTokenInvalid() {
|
void getAttendeeNamesThrowsWhenOrganizerTokenInvalid() {
|
||||||
Event event = buildActiveEvent();
|
Event event = buildActiveEvent(TODAY.plusDays(30));
|
||||||
EventToken token = event.getEventToken();
|
EventToken token = event.eventToken();
|
||||||
OrganizerToken wrongToken = OrganizerToken.generate();
|
OrganizerToken wrongToken = OrganizerToken.generate();
|
||||||
when(eventRepository.findByEventToken(token))
|
when(eventRepository.findByEventToken(token))
|
||||||
.thenReturn(Optional.of(event));
|
.thenReturn(Optional.of(event));
|
||||||
@@ -183,38 +185,33 @@ class RsvpServiceTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Rsvp buildRsvp(Long id, String name) {
|
private Rsvp buildRsvp(Long id, String name) {
|
||||||
var rsvp = new Rsvp();
|
return new Rsvp(id, RsvpToken.generate(), 1L, name);
|
||||||
rsvp.setId(id);
|
|
||||||
rsvp.setRsvpToken(RsvpToken.generate());
|
|
||||||
rsvp.setEventId(1L);
|
|
||||||
rsvp.setName(name);
|
|
||||||
return rsvp;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void cancelRsvpDeletesWhenEventAndRsvpExist() {
|
void cancelRsvpDeletesWhenEventAndRsvpExist() {
|
||||||
Event event = buildActiveEvent();
|
Event event = buildActiveEvent(TODAY.plusDays(30));
|
||||||
EventToken token = event.getEventToken();
|
EventToken token = event.eventToken();
|
||||||
RsvpToken rsvpToken = RsvpToken.generate();
|
RsvpToken rsvpToken = RsvpToken.generate();
|
||||||
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
||||||
when(rsvpRepository.deleteByEventIdAndRsvpToken(event.getId(), rsvpToken)).thenReturn(true);
|
when(rsvpRepository.deleteByEventIdAndRsvpToken(event.id(), rsvpToken)).thenReturn(true);
|
||||||
|
|
||||||
rsvpService.cancelRsvp(token, rsvpToken);
|
rsvpService.cancelRsvp(token, rsvpToken);
|
||||||
|
|
||||||
verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.getId(), rsvpToken);
|
verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.id(), rsvpToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void cancelRsvpSucceedsWhenRsvpNotFound() {
|
void cancelRsvpSucceedsWhenRsvpNotFound() {
|
||||||
Event event = buildActiveEvent();
|
Event event = buildActiveEvent(TODAY.plusDays(30));
|
||||||
EventToken token = event.getEventToken();
|
EventToken token = event.eventToken();
|
||||||
RsvpToken rsvpToken = RsvpToken.generate();
|
RsvpToken rsvpToken = RsvpToken.generate();
|
||||||
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
when(eventRepository.findByEventToken(token)).thenReturn(Optional.of(event));
|
||||||
when(rsvpRepository.deleteByEventIdAndRsvpToken(event.getId(), rsvpToken)).thenReturn(false);
|
when(rsvpRepository.deleteByEventIdAndRsvpToken(event.id(), rsvpToken)).thenReturn(false);
|
||||||
|
|
||||||
rsvpService.cancelRsvp(token, rsvpToken);
|
rsvpService.cancelRsvp(token, rsvpToken);
|
||||||
|
|
||||||
verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.getId(), rsvpToken);
|
verify(rsvpRepository).deleteByEventIdAndRsvpToken(event.id(), rsvpToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -226,16 +223,19 @@ class RsvpServiceTest {
|
|||||||
rsvpService.cancelRsvp(token, rsvpToken);
|
rsvpService.cancelRsvp(token, rsvpToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Event buildActiveEvent() {
|
private Event buildActiveEvent(LocalDate expiryDate) {
|
||||||
var event = new Event();
|
return new Event(
|
||||||
event.setId(1L);
|
1L,
|
||||||
event.setEventToken(EventToken.generate());
|
EventToken.generate(),
|
||||||
event.setOrganizerToken(OrganizerToken.generate());
|
OrganizerToken.generate(),
|
||||||
event.setTitle("Test Event");
|
"Test Event",
|
||||||
event.setDateTime(OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)));
|
null,
|
||||||
event.setTimezone(ZONE);
|
OffsetDateTime.of(2026, 6, 15, 20, 0, 0, 0, ZoneOffset.ofHours(2)),
|
||||||
event.setExpiryDate(TODAY.plusDays(30));
|
ZONE,
|
||||||
event.setCreatedAt(OffsetDateTime.now());
|
null,
|
||||||
return event;
|
expiryDate,
|
||||||
|
OffsetDateTime.now(),
|
||||||
|
false,
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user