feat(transcription): person @mention sidecar + rename propagation (PR-A backend, #362) #366
@@ -61,7 +61,7 @@ public class PersonMentionPropagationListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
blockRepository.saveAll(blocks);
|
blockRepository.saveAllAndFlush(blocks);
|
||||||
|
|
||||||
log.info("Propagated rename {} → {} across {} block(s) for person {}",
|
log.info("Propagated rename {} → {} across {} block(s) for person {}",
|
||||||
event.oldDisplayName(), event.newDisplayName(), blocks.size(), event.personId());
|
event.oldDisplayName(), event.newDisplayName(), blocks.size(), event.personId());
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.raddatz.familienarchiv.model.PersonType;
|
|||||||
import org.raddatz.familienarchiv.repository.PersonNameAliasRepository;
|
import org.raddatz.familienarchiv.repository.PersonNameAliasRepository;
|
||||||
import org.raddatz.familienarchiv.repository.PersonRepository;
|
import org.raddatz.familienarchiv.repository.PersonRepository;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -176,7 +177,12 @@ public class PersonService {
|
|||||||
Person saved = personRepository.save(person);
|
Person saved = personRepository.save(person);
|
||||||
String newDisplayName = saved.getDisplayName();
|
String newDisplayName = saved.getDisplayName();
|
||||||
if (!Objects.equals(oldDisplayName, newDisplayName)) {
|
if (!Objects.equals(oldDisplayName, newDisplayName)) {
|
||||||
eventPublisher.publishEvent(new PersonDisplayNameChangedEvent(id, oldDisplayName, newDisplayName));
|
try {
|
||||||
|
eventPublisher.publishEvent(new PersonDisplayNameChangedEvent(id, oldDisplayName, newDisplayName));
|
||||||
|
} catch (OptimisticLockingFailureException e) {
|
||||||
|
throw DomainException.conflict(ErrorCode.PERSON_RENAME_CONFLICT,
|
||||||
|
"A referenced transcription block was modified concurrently — rename rolled back");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.raddatz.familienarchiv.dto.PersonNameAliasDTO;
|
|||||||
import org.raddatz.familienarchiv.dto.PersonSummaryDTO;
|
import org.raddatz.familienarchiv.dto.PersonSummaryDTO;
|
||||||
import org.raddatz.familienarchiv.dto.PersonUpdateDTO;
|
import org.raddatz.familienarchiv.dto.PersonUpdateDTO;
|
||||||
import org.raddatz.familienarchiv.exception.DomainException;
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
import org.raddatz.familienarchiv.model.Person;
|
import org.raddatz.familienarchiv.model.Person;
|
||||||
import org.raddatz.familienarchiv.model.PersonDisplayNameChangedEvent;
|
import org.raddatz.familienarchiv.model.PersonDisplayNameChangedEvent;
|
||||||
import org.raddatz.familienarchiv.model.PersonNameAlias;
|
import org.raddatz.familienarchiv.model.PersonNameAlias;
|
||||||
@@ -18,6 +19,7 @@ import org.raddatz.familienarchiv.model.PersonType;
|
|||||||
import org.raddatz.familienarchiv.repository.PersonNameAliasRepository;
|
import org.raddatz.familienarchiv.repository.PersonNameAliasRepository;
|
||||||
import org.raddatz.familienarchiv.repository.PersonRepository;
|
import org.raddatz.familienarchiv.repository.PersonRepository;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.dao.OptimisticLockingFailureException;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -297,6 +299,28 @@ class PersonServiceTest {
|
|||||||
verify(eventPublisher, never()).publishEvent(any(PersonDisplayNameChangedEvent.class));
|
verify(eventPublisher, never()).publishEvent(any(PersonDisplayNameChangedEvent.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updatePerson_throwsConflict_whenListenerSignalsOptimisticLockFailure() {
|
||||||
|
UUID id = UUID.randomUUID();
|
||||||
|
Person existing = Person.builder()
|
||||||
|
.id(id).firstName("Auguste").lastName("Raddatz")
|
||||||
|
.personType(PersonType.PERSON).build();
|
||||||
|
|
||||||
|
when(personRepository.findById(id)).thenReturn(Optional.of(existing));
|
||||||
|
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||||
|
doThrow(new OptimisticLockingFailureException("simulated concurrent block save"))
|
||||||
|
.when(eventPublisher).publishEvent(any(PersonDisplayNameChangedEvent.class));
|
||||||
|
|
||||||
|
PersonUpdateDTO dto = new PersonUpdateDTO();
|
||||||
|
dto.setPersonType(PersonType.PERSON);
|
||||||
|
dto.setFirstName("Augusta"); dto.setLastName("Raddatz");
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> personService.updatePerson(id, dto))
|
||||||
|
.isInstanceOf(DomainException.class)
|
||||||
|
.matches(e -> ((DomainException) e).getCode() == ErrorCode.PERSON_RENAME_CONFLICT)
|
||||||
|
.matches(e -> ((DomainException) e).getStatus().value() == 409);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void updatePerson_doesNotPublishEvent_whenOnlyNotesChanges() {
|
void updatePerson_doesNotPublishEvent_whenOnlyNotesChanges() {
|
||||||
UUID id = UUID.randomUUID();
|
UUID id = UUID.randomUUID();
|
||||||
|
|||||||
Reference in New Issue
Block a user