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 1d24f3fc..689f6726 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEntryDTO.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEntryDTO.java @@ -35,6 +35,11 @@ import java.util.UUID; * entries. Deliberately NOT {@code @Schema(requiredMode = REQUIRED)} so the generated TypeScript * type stays optional. * + *

Event description ({@code description}): curator-authored context note for a curated + * {@link Kind#EVENT} entry (#844). Populated from {@link TimelineEvent#getDescription()} — null + * for {@link Kind#LETTER} and derived entries. Deliberately NOT + * {@code @Schema(requiredMode = REQUIRED)} so the generated TypeScript type stays optional. + * *

Callers of {@code TimelineEventService.assembleDerivedEvents()} must independently enforce * {@code READ_ALL} authorization before invoking that method (see ADR-043). */ @@ -55,6 +60,7 @@ public record TimelineEntryDTO( UUID rootTagId, String rootTagName, String rootTagColor, - UUID linkedEventId + UUID linkedEventId, + String description ) { } 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 03092647..4de1107a 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEventService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineEventService.java @@ -267,7 +267,7 @@ public class TimelineEventService { p.getBirthDate(), null, p.getDisplayName(), EventType.PERSONAL, null, null, List.of(p.getId()), DerivedEventType.BIRTH, - null, null, null, null)) + null, null, null, null, null)) .toList(); } @@ -279,7 +279,7 @@ public class TimelineEventService { p.getDeathDate(), null, p.getDisplayName(), EventType.PERSONAL, null, null, List.of(p.getId()), DerivedEventType.DEATH, - null, null, null, null)) + null, null, null, null, null)) .toList(); } @@ -304,7 +304,7 @@ public class TimelineEventService { null, null, List.of(r.getPerson().getId(), r.getRelatedPerson().getId()), DerivedEventType.MARRIAGE, - null, null, null, null)); + null, null, null, null, null)); } } return result; diff --git a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineService.java b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineService.java index 75dc381a..3404ddc3 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/timeline/TimelineService.java @@ -238,7 +238,8 @@ public class TimelineService { null, null, null, - null + null, + ev.getDescription() ); } @@ -262,7 +263,8 @@ public class TimelineService { root == null ? null : root.id(), root == null ? null : root.name(), root == null ? null : root.color(), - eventByDocId.get(doc.getId()) + eventByDocId.get(doc.getId()), + null ); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/timeline/TimelineControllerTest.java b/backend/src/test/java/org/raddatz/familienarchiv/timeline/TimelineControllerTest.java index 61c81496..405aa55b 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/timeline/TimelineControllerTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/timeline/TimelineControllerTest.java @@ -74,6 +74,28 @@ class TimelineControllerTest { .andExpect(jsonPath("$.undated").isArray()); } + // ─── REQ-001: description field serialised ─────────────────────────────── + + @Test + @org.springframework.security.test.context.support.WithMockUser(authorities = "READ_ALL") + void timelineIncludesEventDescription() throws Exception { + // REQ-001 (controller slice): a curated event entry with description "Kontext" is + // serialised into the timeline response at the correct JSON path. + var entry = new TimelineEntryDTO(Kind.EVENT, org.raddatz.familienarchiv.document.DatePrecision.DAY, + false, "", "", + java.time.LocalDate.of(1914, 8, 1), null, "Kriegsbeginn", + EventType.HISTORICAL, UUID.randomUUID(), null, List.of(), null, + null, null, null, null, "Kontext"); + when(timelineService.assemble(any())) + .thenReturn(new TimelineDTO( + List.of(new TimelineYearDTO(1914, List.of(entry))), + List.of())); + + mockMvc.perform(get("/api/timeline")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.years[0].entries[0].description", is("Kontext"))); + } + // ─── Parameter binding ──────────────────────────────────────────────────── @Test diff --git a/backend/src/test/java/org/raddatz/familienarchiv/timeline/TimelineServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/timeline/TimelineServiceTest.java index b146f66e..3a8d06c9 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/timeline/TimelineServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/timeline/TimelineServiceTest.java @@ -69,10 +69,10 @@ class TimelineServiceTest { UUID id2 = UUID.fromString("00000000-0000-0000-0000-000000000002"); var e1 = new TimelineEntryDTO(Kind.LETTER, DatePrecision.DAY, false, "", "", LocalDate.of(1914, 7, 28), null, "Same Title", null, null, id1, List.of(), null, - null, null, null, null); + null, null, null, null, null); var e2 = new TimelineEntryDTO(Kind.LETTER, DatePrecision.DAY, false, "", "", LocalDate.of(1914, 7, 28), null, "Same Title", null, null, id2, List.of(), null, - null, null, null, null); + null, null, null, null, null); var sorted = List.of(e2, e1).stream() .sorted(TimelineService.WITHIN_BAND_ORDER) @@ -511,6 +511,53 @@ class TimelineServiceTest { verify(tagService, times(1)).resolveRootTags(anyList()); } + // ─── event description (#844, REQ-001) ─────────────────────────────────── + + @Test + void mapEvent_populates_description_from_event() { + // REQ-001: a curated event with a description surfaces it on the assembled entry. + TimelineEvent ev = TimelineEvent.builder().id(UUID.randomUUID()) + .title("Kriegsbeginn").type(EventType.HISTORICAL) + .eventDate(LocalDate.of(1914, 8, 1)).precision(DatePrecision.DAY) + .description("Kontext") + .build(); + when(eventRepository.findAll()).thenReturn(List.of(ev)); + when(timelineEventService.assembleDerivedEvents()).thenReturn(List.of()); + when(documentService.getAllForTimeline()).thenReturn(List.of()); + + TimelineDTO result = timelineService.assemble(noFilters()); + + TimelineEntryDTO entry = result.years().get(0).entries().get(0); + assertThat(entry.description()).isEqualTo("Kontext"); + } + + @Test + void mapEvent_leaves_description_null_when_event_has_none() { + // REQ-001: an event without a description → null on the entry. + TimelineEvent ev = TimelineEvent.builder().id(UUID.randomUUID()) + .title("Ereignis").type(EventType.PERSONAL) + .eventDate(LocalDate.of(1920, 1, 1)).precision(DatePrecision.YEAR) + .build(); + when(eventRepository.findAll()).thenReturn(List.of(ev)); + when(timelineEventService.assembleDerivedEvents()).thenReturn(List.of()); + when(documentService.getAllForTimeline()).thenReturn(List.of()); + + TimelineEntryDTO entry = timelineService.assemble(noFilters()).years().get(0).entries().get(0); + assertThat(entry.description()).isNull(); + } + + @Test + void mapDocument_leaves_description_null_for_letter() { + // REQ-001: LETTER entries carry null description, regardless of any document fields. + Document doc = docWithDate(LocalDate.of(1916, 3, 1), DatePrecision.MONTH); + when(eventRepository.findAll()).thenReturn(List.of()); + when(timelineEventService.assembleDerivedEvents()).thenReturn(List.of()); + when(documentService.getAllForTimeline()).thenReturn(List.of(doc)); + + TimelineEntryDTO entry = onlyLetterEntry(timelineService.assemble(noFilters())); + assertThat(entry.description()).isNull(); + } + // ─── letter→event link (#850, REQ-009) ─────────────────────────────────── @Test @@ -623,7 +670,7 @@ class TimelineServiceTest { private static TimelineEntryDTO letter(LocalDate date, DatePrecision precision, String title) { return new TimelineEntryDTO(Kind.LETTER, precision, false, "", "", date, null, title, null, null, UUID.randomUUID(), List.of(), null, - null, null, null, null); + null, null, null, null, null); } private static Document docWithDate(LocalDate date, DatePrecision precision) {