From 184fc9814a979b46dc8d953df6f70190ef5a6e6a Mon Sep 17 00:00:00 2001 From: Marcel Date: Sat, 13 Jun 2026 16:03:00 +0200 Subject: [PATCH] refactor(timeline): adapt TimelineEntryDTO to unified #777 shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the #776 DTO (primary/relatedPersonName + synthetic String id) with the full #777 spec: kind, senderName, receiverName, eventId, documentId, linkedPersonIds, title, eventDateEnd. Derived events now use title=displayName, linkedPersonIds=[UUID...], eventId=null. DerivedEventsAssemblyTest updated — all 16 tests pass. Refs #777 Co-Authored-By: Claude Sonnet 4.6 --- .../raddatz/familienarchiv/timeline/Kind.java | 7 ++++ .../timeline/TimelineEntryDTO.java | 39 +++++++++++------- .../timeline/TimelineEventService.java | 40 ++++++++----------- .../timeline/DerivedEventsAssemblyTest.java | 17 ++++---- 4 files changed, 56 insertions(+), 47 deletions(-) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/timeline/Kind.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/timeline/Kind.java b/backend/src/main/java/org/raddatz/familienarchiv/timeline/Kind.java new file mode 100644 index 00000000..4ac07440 --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/timeline/Kind.java @@ -0,0 +1,7 @@ +package org.raddatz.familienarchiv.timeline; + +/** Discriminates curated/derived events from archive letters in {@link TimelineEntryDTO}. */ +public enum Kind { + EVENT, + LETTER +} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEntryDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEntryDTO.java index 9f729be9..44cf88ec 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEntryDTO.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEntryDTO.java @@ -4,28 +4,39 @@ import io.swagger.v3.oas.annotations.media.Schema; import org.raddatz.familienarchiv.document.DatePrecision; import java.time.LocalDate; +import java.util.List; +import java.util.UUID; /** - * Unified DTO for timeline entries — covers both curated {@link TimelineEvent} rows - * ({@code derived=false}) and derived life-events assembled from Person/relationship data - * ({@code derived=true}). + * Unified DTO for timeline entries — covers curated {@link TimelineEvent} rows, derived + * life-events ({@link DerivedEventType}), and archive letters (Documents). * - *

The {@code id} field is typed {@code String}, not {@code UUID}, because derived events - * carry synthetic prefixed ids ({@code birth:{uuid}}, {@code death:{uuid}}, - * {@code marriage:{uuid}}) that are structurally non-UUID by construction. Any write endpoint - * must reject ids that do not parse as {@code UUID} — enforced and tested in issue #5. + *

Edit-affordance contract (for issue #7): {@code derived == true || eventId == null} + * means no edit link should be rendered by the frontend. * - *

Callers of {@code TimelineService.assembleDerivedEvents()} must independently enforce + *

Letter display fields: {@code senderName} — {@code ""} means unknown/unlinked + * correspondent; frontend renders {@code 'Unbekannt'} fallback. Only populated for + * {@link Kind#LETTER} entries. + * + *

Type field: {@code null} for {@link Kind#LETTER} entries; frontend must not render + * an event-type badge for letters. + * + *

Callers of {@code TimelineEventService.assembleDerivedEvents()} must independently enforce * {@code READ_ALL} authorization before invoking that method (see ADR-043). */ public record TimelineEntryDTO( - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String id, - @Schema(requiredMode = Schema.RequiredMode.REQUIRED) EventType type, - LocalDate eventDate, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) Kind kind, @Schema(requiredMode = Schema.RequiredMode.REQUIRED) DatePrecision precision, @Schema(requiredMode = Schema.RequiredMode.REQUIRED) boolean derived, - DerivedEventType derivedType, - String primaryPersonName, - String relatedPersonName + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String senderName, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String receiverName, + LocalDate eventDate, + LocalDate eventDateEnd, + String title, + EventType type, + UUID eventId, + UUID documentId, + List linkedPersonIds, + DerivedEventType derivedType ) { } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEventService.java b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEventService.java index 3a50d4bf..75803bb0 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEventService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEventService.java @@ -263,14 +263,10 @@ public class TimelineEventService { return persons.stream() .filter(p -> p.getBirthDate() != null) .map(p -> new TimelineEntryDTO( - "birth:" + p.getId(), - EventType.PERSONAL, - p.getBirthDate(), - p.getBirthDatePrecision(), - true, - DerivedEventType.BIRTH, - p.getDisplayName(), - null)) + Kind.EVENT, p.getBirthDatePrecision(), true, "", "", + p.getBirthDate(), null, + p.getDisplayName(), EventType.PERSONAL, + null, null, List.of(p.getId()), DerivedEventType.BIRTH)) .toList(); } @@ -278,14 +274,10 @@ public class TimelineEventService { return persons.stream() .filter(p -> p.getDeathDate() != null) .map(p -> new TimelineEntryDTO( - "death:" + p.getId(), - EventType.PERSONAL, - p.getDeathDate(), - p.getDeathDatePrecision(), - true, - DerivedEventType.DEATH, - p.getDisplayName(), - null)) + Kind.EVENT, p.getDeathDatePrecision(), true, "", "", + p.getDeathDate(), null, + p.getDisplayName(), EventType.PERSONAL, + null, null, List.of(p.getId()), DerivedEventType.DEATH)) .toList(); } @@ -303,15 +295,15 @@ public class TimelineEventService { DatePrecision precision = r.getFromYear() != null ? DatePrecision.YEAR : DatePrecision.UNKNOWN; + String title = r.getPerson().getDisplayName() + + " & " + r.getRelatedPerson().getDisplayName(); result.add(new TimelineEntryDTO( - "marriage:" + r.getId(), - EventType.PERSONAL, - eventDate, - precision, - true, - DerivedEventType.MARRIAGE, - r.getPerson().getDisplayName(), - r.getRelatedPerson().getDisplayName())); + Kind.EVENT, precision, true, "", "", + eventDate, null, + title, EventType.PERSONAL, + null, null, + List.of(r.getPerson().getId(), r.getRelatedPerson().getId()), + DerivedEventType.MARRIAGE)); } } return result; diff --git a/backend/src/test/java/org/raddatz/familienarchiv/timeline/DerivedEventsAssemblyTest.java b/backend/src/test/java/org/raddatz/familienarchiv/timeline/DerivedEventsAssemblyTest.java index 9566b1fd..ddef7bd6 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/timeline/DerivedEventsAssemblyTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/timeline/DerivedEventsAssemblyTest.java @@ -18,7 +18,6 @@ import java.util.List; import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -108,7 +107,7 @@ class DerivedEventsAssemblyTest { assertThat(event.derivedType()).isEqualTo(DerivedEventType.BIRTH); assertThat(event.eventDate()).isEqualTo(LocalDate.of(1901, 3, 12)); assertThat(event.precision()).isEqualTo(DatePrecision.DAY); - assertThat(event.primaryPersonName()).isEqualTo(anna.getDisplayName()); + assertThat(event.title()).isEqualTo(anna.getDisplayName()); } // --- REQ-003: null birthDate → no Geburt event --- @@ -162,7 +161,7 @@ class DerivedEventsAssemblyTest { assertThat(event.derivedType()).isEqualTo(DerivedEventType.DEATH); assertThat(event.eventDate()).isEqualTo(LocalDate.of(1965, 7, 4)); assertThat(event.precision()).isEqualTo(DatePrecision.DAY); - assertThat(event.primaryPersonName()).isEqualTo(hans.getDisplayName()); + assertThat(event.title()).isEqualTo(hans.getDisplayName()); } // --- REQ-002 + REQ-003 combined --- @@ -285,10 +284,10 @@ class DerivedEventsAssemblyTest { List result = service.assembleDerivedEvents(); assertThat(result).hasSize(1); - String id = result.get(0).id(); - assertThat(id).startsWith("birth:"); - assertThatThrownBy(() -> UUID.fromString(id)) - .isInstanceOf(IllegalArgumentException.class); + TimelineEntryDTO entry = result.get(0); + assertThat(entry.derived()).isTrue(); + assertThat(entry.eventId()).isNull(); + assertThat(entry.documentId()).isNull(); } // --- REQ-010: display names on Heirat --- @@ -307,8 +306,8 @@ class DerivedEventsAssemblyTest { assertThat(heiraten).hasSize(1); TimelineEntryDTO heirat = heiraten.get(0); - assertThat(heirat.primaryPersonName()).isNotNull().isNotBlank(); - assertThat(heirat.relatedPersonName()).isNotNull().isNotBlank(); + assertThat(heirat.title()).isNotNull().isNotBlank(); + assertThat(heirat.linkedPersonIds()).hasSize(2); } // --- REQ-007 note: assumption/documentation test ---