From 08e7987033d0f4d563b4a99bdd7d4e261225ba51 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 28 Apr 2026 20:17:17 +0200 Subject: [PATCH] feat(person): updatePerson publishes PersonDisplayNameChangedEvent on display-name change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PersonService now emits a domain event whenever Person.getDisplayName() flips during an update. The snapshot is taken before the setter chain so we compare like-for-like against the post-save value, and the event only publishes when the two strings differ. The test captures the published event via ArgumentCaptor and asserts the title flip from "Herr" to "Frau" reaches the publisher with the correct personId, oldDisplayName, and newDisplayName. Title participates in DisplayNameFormatter, so this is the canonical case for "rename triggered by something other than first/last name." Implements PR-A tasks 9 and 10 as one red→green cycle (the test drove the production change). Subsequent commits cover the negative cases (alias / notes only) and the propagation listener that consumes the event. Refs #362 Co-Authored-By: Claude Opus 4.7 --- .../familienarchiv/service/PersonService.java | 11 +++++- .../service/PersonServiceTest.java | 35 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java index df04aa38..c83050ac 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/PersonService.java @@ -13,11 +13,13 @@ import org.raddatz.familienarchiv.dto.PersonUpdateDTO; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.model.Person; +import org.raddatz.familienarchiv.model.PersonDisplayNameChangedEvent; import org.raddatz.familienarchiv.model.PersonNameAlias; import org.raddatz.familienarchiv.model.PersonNameAliasType; import org.raddatz.familienarchiv.model.PersonType; import org.raddatz.familienarchiv.repository.PersonNameAliasRepository; import org.raddatz.familienarchiv.repository.PersonRepository; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -31,6 +33,7 @@ public class PersonService { private final PersonRepository personRepository; private final PersonNameAliasRepository aliasRepository; + private final ApplicationEventPublisher eventPublisher; public List findAll(String q) { if (q == null) { @@ -157,6 +160,7 @@ public class PersonService { validateYears(dto.getBirthYear(), dto.getDeathYear()); Person person = personRepository.findById(id) .orElseThrow(() -> DomainException.notFound(ErrorCode.PERSON_NOT_FOUND, "Person not found: " + id)); + String oldDisplayName = person.getDisplayName(); person.setPersonType(dto.getPersonType()); person.setTitle(dto.getTitle() == null || dto.getTitle().isBlank() ? null : dto.getTitle().trim()); person.setFirstName(dto.getFirstName()); @@ -165,7 +169,12 @@ public class PersonService { person.setNotes(dto.getNotes() == null || dto.getNotes().isBlank() ? null : dto.getNotes().trim()); person.setBirthYear(dto.getBirthYear()); person.setDeathYear(dto.getDeathYear()); - return personRepository.save(person); + Person saved = personRepository.save(person); + String newDisplayName = saved.getDisplayName(); + if (!Objects.equals(oldDisplayName, newDisplayName)) { + eventPublisher.publishEvent(new PersonDisplayNameChangedEvent(id, oldDisplayName, newDisplayName)); + } + return saved; } @Transactional diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/PersonServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/PersonServiceTest.java index da2fdde4..3351fd61 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/PersonServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/PersonServiceTest.java @@ -2,6 +2,7 @@ package org.raddatz.familienarchiv.service; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; @@ -10,11 +11,13 @@ import org.raddatz.familienarchiv.dto.PersonSummaryDTO; import org.raddatz.familienarchiv.dto.PersonUpdateDTO; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.model.Person; +import org.raddatz.familienarchiv.model.PersonDisplayNameChangedEvent; import org.raddatz.familienarchiv.model.PersonNameAlias; import org.raddatz.familienarchiv.model.PersonNameAliasType; import org.raddatz.familienarchiv.model.PersonType; import org.raddatz.familienarchiv.repository.PersonNameAliasRepository; import org.raddatz.familienarchiv.repository.PersonRepository; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.web.server.ResponseStatusException; import java.util.List; @@ -31,6 +34,7 @@ class PersonServiceTest { @Mock PersonRepository personRepository; @Mock PersonNameAliasRepository aliasRepository; + @Mock ApplicationEventPublisher eventPublisher; @InjectMocks PersonService personService; // ─── getById ───────────────────────────────────────────────────────────── @@ -242,6 +246,37 @@ class PersonServiceTest { assertThat(result.getAlias()).isEqualTo("Anna Alt"); } + // ─── updatePerson (display-name change event) ──────────────────────────── + + @Test + void updatePerson_publishesEvent_whenTitleChanges() { + UUID id = UUID.randomUUID(); + Person existing = Person.builder() + .id(id).title("Herr").firstName("Auguste").lastName("Raddatz") + .personType(PersonType.PERSON).build(); + String oldName = existing.getDisplayName(); + + when(personRepository.findById(id)).thenReturn(Optional.of(existing)); + when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + PersonUpdateDTO dto = new PersonUpdateDTO(); + dto.setPersonType(PersonType.PERSON); + dto.setTitle("Frau"); dto.setFirstName("Auguste"); dto.setLastName("Raddatz"); + + personService.updatePerson(id, dto); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PersonDisplayNameChangedEvent.class); + verify(eventPublisher).publishEvent(captor.capture()); + + PersonDisplayNameChangedEvent event = captor.getValue(); + assertThat(event.personId()).isEqualTo(id); + assertThat(event.oldDisplayName()).isEqualTo(oldName); + assertThat(event.newDisplayName()) + .isNotEqualTo(oldName) + .contains("Frau"); + } + // ─── findOrCreateByAlias ───────────────────────────────────────────────── @Test