refactor(timeline): adapt TimelineEntryDTO to unified #777 shape
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 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
package org.raddatz.familienarchiv.timeline;
|
||||||
|
|
||||||
|
/** Discriminates curated/derived events from archive letters in {@link TimelineEntryDTO}. */
|
||||||
|
public enum Kind {
|
||||||
|
EVENT,
|
||||||
|
LETTER
|
||||||
|
}
|
||||||
@@ -4,28 +4,39 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
import org.raddatz.familienarchiv.document.DatePrecision;
|
import org.raddatz.familienarchiv.document.DatePrecision;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unified DTO for timeline entries — covers both curated {@link TimelineEvent} rows
|
* Unified DTO for timeline entries — covers curated {@link TimelineEvent} rows, derived
|
||||||
* ({@code derived=false}) and derived life-events assembled from Person/relationship data
|
* life-events ({@link DerivedEventType}), and archive letters (Documents).
|
||||||
* ({@code derived=true}).
|
|
||||||
*
|
*
|
||||||
* <p>The {@code id} field is typed {@code String}, not {@code UUID}, because derived events
|
* <p><b>Edit-affordance contract (for issue #7):</b> {@code derived == true || eventId == null}
|
||||||
* carry synthetic prefixed ids ({@code birth:{uuid}}, {@code death:{uuid}},
|
* means no edit link should be rendered by the frontend.
|
||||||
* {@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.
|
|
||||||
*
|
*
|
||||||
* <p>Callers of {@code TimelineService.assembleDerivedEvents()} must independently enforce
|
* <p><b>Letter display fields:</b> {@code senderName} — {@code ""} means unknown/unlinked
|
||||||
|
* correspondent; frontend renders {@code 'Unbekannt'} fallback. Only populated for
|
||||||
|
* {@link Kind#LETTER} entries.
|
||||||
|
*
|
||||||
|
* <p><b>Type field:</b> {@code null} for {@link Kind#LETTER} entries; frontend must not render
|
||||||
|
* an event-type badge for letters.
|
||||||
|
*
|
||||||
|
* <p>Callers of {@code TimelineEventService.assembleDerivedEvents()} must independently enforce
|
||||||
* {@code READ_ALL} authorization before invoking that method (see ADR-043).
|
* {@code READ_ALL} authorization before invoking that method (see ADR-043).
|
||||||
*/
|
*/
|
||||||
public record TimelineEntryDTO(
|
public record TimelineEntryDTO(
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String id,
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) Kind kind,
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) EventType type,
|
|
||||||
LocalDate eventDate,
|
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) DatePrecision precision,
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) DatePrecision precision,
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) boolean derived,
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) boolean derived,
|
||||||
DerivedEventType derivedType,
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String senderName,
|
||||||
String primaryPersonName,
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String receiverName,
|
||||||
String relatedPersonName
|
LocalDate eventDate,
|
||||||
|
LocalDate eventDateEnd,
|
||||||
|
String title,
|
||||||
|
EventType type,
|
||||||
|
UUID eventId,
|
||||||
|
UUID documentId,
|
||||||
|
List<UUID> linkedPersonIds,
|
||||||
|
DerivedEventType derivedType
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,14 +263,10 @@ public class TimelineEventService {
|
|||||||
return persons.stream()
|
return persons.stream()
|
||||||
.filter(p -> p.getBirthDate() != null)
|
.filter(p -> p.getBirthDate() != null)
|
||||||
.map(p -> new TimelineEntryDTO(
|
.map(p -> new TimelineEntryDTO(
|
||||||
"birth:" + p.getId(),
|
Kind.EVENT, p.getBirthDatePrecision(), true, "", "",
|
||||||
EventType.PERSONAL,
|
p.getBirthDate(), null,
|
||||||
p.getBirthDate(),
|
p.getDisplayName(), EventType.PERSONAL,
|
||||||
p.getBirthDatePrecision(),
|
null, null, List.of(p.getId()), DerivedEventType.BIRTH))
|
||||||
true,
|
|
||||||
DerivedEventType.BIRTH,
|
|
||||||
p.getDisplayName(),
|
|
||||||
null))
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,14 +274,10 @@ public class TimelineEventService {
|
|||||||
return persons.stream()
|
return persons.stream()
|
||||||
.filter(p -> p.getDeathDate() != null)
|
.filter(p -> p.getDeathDate() != null)
|
||||||
.map(p -> new TimelineEntryDTO(
|
.map(p -> new TimelineEntryDTO(
|
||||||
"death:" + p.getId(),
|
Kind.EVENT, p.getDeathDatePrecision(), true, "", "",
|
||||||
EventType.PERSONAL,
|
p.getDeathDate(), null,
|
||||||
p.getDeathDate(),
|
p.getDisplayName(), EventType.PERSONAL,
|
||||||
p.getDeathDatePrecision(),
|
null, null, List.of(p.getId()), DerivedEventType.DEATH))
|
||||||
true,
|
|
||||||
DerivedEventType.DEATH,
|
|
||||||
p.getDisplayName(),
|
|
||||||
null))
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,15 +295,15 @@ public class TimelineEventService {
|
|||||||
DatePrecision precision = r.getFromYear() != null
|
DatePrecision precision = r.getFromYear() != null
|
||||||
? DatePrecision.YEAR
|
? DatePrecision.YEAR
|
||||||
: DatePrecision.UNKNOWN;
|
: DatePrecision.UNKNOWN;
|
||||||
|
String title = r.getPerson().getDisplayName()
|
||||||
|
+ " & " + r.getRelatedPerson().getDisplayName();
|
||||||
result.add(new TimelineEntryDTO(
|
result.add(new TimelineEntryDTO(
|
||||||
"marriage:" + r.getId(),
|
Kind.EVENT, precision, true, "", "",
|
||||||
EventType.PERSONAL,
|
eventDate, null,
|
||||||
eventDate,
|
title, EventType.PERSONAL,
|
||||||
precision,
|
null, null,
|
||||||
true,
|
List.of(r.getPerson().getId(), r.getRelatedPerson().getId()),
|
||||||
DerivedEventType.MARRIAGE,
|
DerivedEventType.MARRIAGE));
|
||||||
r.getPerson().getDisplayName(),
|
|
||||||
r.getRelatedPerson().getDisplayName()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import java.util.List;
|
|||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
@@ -108,7 +107,7 @@ class DerivedEventsAssemblyTest {
|
|||||||
assertThat(event.derivedType()).isEqualTo(DerivedEventType.BIRTH);
|
assertThat(event.derivedType()).isEqualTo(DerivedEventType.BIRTH);
|
||||||
assertThat(event.eventDate()).isEqualTo(LocalDate.of(1901, 3, 12));
|
assertThat(event.eventDate()).isEqualTo(LocalDate.of(1901, 3, 12));
|
||||||
assertThat(event.precision()).isEqualTo(DatePrecision.DAY);
|
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 ---
|
// --- REQ-003: null birthDate → no Geburt event ---
|
||||||
@@ -162,7 +161,7 @@ class DerivedEventsAssemblyTest {
|
|||||||
assertThat(event.derivedType()).isEqualTo(DerivedEventType.DEATH);
|
assertThat(event.derivedType()).isEqualTo(DerivedEventType.DEATH);
|
||||||
assertThat(event.eventDate()).isEqualTo(LocalDate.of(1965, 7, 4));
|
assertThat(event.eventDate()).isEqualTo(LocalDate.of(1965, 7, 4));
|
||||||
assertThat(event.precision()).isEqualTo(DatePrecision.DAY);
|
assertThat(event.precision()).isEqualTo(DatePrecision.DAY);
|
||||||
assertThat(event.primaryPersonName()).isEqualTo(hans.getDisplayName());
|
assertThat(event.title()).isEqualTo(hans.getDisplayName());
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- REQ-002 + REQ-003 combined ---
|
// --- REQ-002 + REQ-003 combined ---
|
||||||
@@ -285,10 +284,10 @@ class DerivedEventsAssemblyTest {
|
|||||||
List<TimelineEntryDTO> result = service.assembleDerivedEvents();
|
List<TimelineEntryDTO> result = service.assembleDerivedEvents();
|
||||||
|
|
||||||
assertThat(result).hasSize(1);
|
assertThat(result).hasSize(1);
|
||||||
String id = result.get(0).id();
|
TimelineEntryDTO entry = result.get(0);
|
||||||
assertThat(id).startsWith("birth:");
|
assertThat(entry.derived()).isTrue();
|
||||||
assertThatThrownBy(() -> UUID.fromString(id))
|
assertThat(entry.eventId()).isNull();
|
||||||
.isInstanceOf(IllegalArgumentException.class);
|
assertThat(entry.documentId()).isNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- REQ-010: display names on Heirat ---
|
// --- REQ-010: display names on Heirat ---
|
||||||
@@ -307,8 +306,8 @@ class DerivedEventsAssemblyTest {
|
|||||||
|
|
||||||
assertThat(heiraten).hasSize(1);
|
assertThat(heiraten).hasSize(1);
|
||||||
TimelineEntryDTO heirat = heiraten.get(0);
|
TimelineEntryDTO heirat = heiraten.get(0);
|
||||||
assertThat(heirat.primaryPersonName()).isNotNull().isNotBlank();
|
assertThat(heirat.title()).isNotNull().isNotBlank();
|
||||||
assertThat(heirat.relatedPersonName()).isNotNull().isNotBlank();
|
assertThat(heirat.linkedPersonIds()).hasSize(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- REQ-007 note: assumption/documentation test ---
|
// --- REQ-007 note: assumption/documentation test ---
|
||||||
|
|||||||
Reference in New Issue
Block a user