feat(person): add upsertBySourceRef with human-edit-preserve precedence
Idempotent person upsert keyed on the normalizer person_id (source_ref), for the Phase-3 canonical importer. Re-import precedence (Resolved decision #1): a non-blank existing field is never overwritten, blank fields are filled from canonical, and provisional is monotonic — once a human confirms a person (false) it never reverts to true. New importer-created persons carry provisional=true; register persons false. Maiden name is stored as a MAIDEN_NAME PersonNameAlias, matching the existing findOrCreateByAlias behaviour. Refs #669 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
package org.raddatz.familienarchiv.person;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.argThat;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class PersonImportUpsertTest {
|
||||
|
||||
@Mock PersonRepository personRepository;
|
||||
@Mock PersonNameAliasRepository aliasRepository;
|
||||
@InjectMocks PersonService personService;
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_insertsNewPerson_whenSourceRefUnknown() {
|
||||
when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.empty());
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("clara-cram").firstName("Clara").lastName("Cram")
|
||||
.personType(PersonType.PERSON).provisional(false).build();
|
||||
|
||||
Person result = personService.upsertBySourceRef(cmd);
|
||||
|
||||
assertThat(result.getSourceRef()).isEqualTo("clara-cram");
|
||||
assertThat(result.getFirstName()).isEqualTo("Clara");
|
||||
assertThat(result.getLastName()).isEqualTo("Cram");
|
||||
assertThat(result.isProvisional()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_updatesInPlace_whenSourceRefExists() {
|
||||
Person existing = Person.builder()
|
||||
.id(UUID.randomUUID()).sourceRef("clara-cram")
|
||||
.firstName("Clara").lastName("Cram").build();
|
||||
when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(existing));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("clara-cram").firstName("Clara").lastName("Cram")
|
||||
.notes("Updated note").personType(PersonType.PERSON).provisional(false).build();
|
||||
|
||||
personService.upsertBySourceRef(cmd);
|
||||
|
||||
verify(personRepository).save(argThat(p -> p.getId().equals(existing.getId())));
|
||||
verify(personRepository, never()).save(argThat(p -> p.getId() == null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_preservesHumanEditedNonBlankFields() {
|
||||
// A human renamed the maiden-name register person and added notes in-app.
|
||||
Person humanEdited = Person.builder()
|
||||
.id(UUID.randomUUID()).sourceRef("clara-cram")
|
||||
.firstName("Klara").lastName("Cram-Müller").notes("Verified by Marcel").build();
|
||||
when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(humanEdited));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("clara-cram").firstName("Clara").lastName("Cram")
|
||||
.notes("Auto note").personType(PersonType.PERSON).provisional(false).build();
|
||||
|
||||
Person result = personService.upsertBySourceRef(cmd);
|
||||
|
||||
// Human edits survive the re-import.
|
||||
assertThat(result.getFirstName()).isEqualTo("Klara");
|
||||
assertThat(result.getLastName()).isEqualTo("Cram-Müller");
|
||||
assertThat(result.getNotes()).isEqualTo("Verified by Marcel");
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_fillsOnlyBlankFields_onReimport() {
|
||||
Person existing = Person.builder()
|
||||
.id(UUID.randomUUID()).sourceRef("clara-cram")
|
||||
.firstName("Clara").lastName("Cram").notes(null).build();
|
||||
when(personRepository.findBySourceRef("clara-cram")).thenReturn(Optional.of(existing));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("clara-cram").firstName("Clara").lastName("Cram")
|
||||
.notes("Nichte von Herbert").personType(PersonType.PERSON).provisional(false).build();
|
||||
|
||||
Person result = personService.upsertBySourceRef(cmd);
|
||||
|
||||
// Blank field gets filled by canonical value.
|
||||
assertThat(result.getNotes()).isEqualTo("Nichte von Herbert");
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_neverFlipsProvisionalBackToTrue_onceHumanConfirmed() {
|
||||
// A human confirmed this provisional importer-created person (provisional -> false).
|
||||
Person confirmed = Person.builder()
|
||||
.id(UUID.randomUUID()).sourceRef("schwester-hanni")
|
||||
.firstName(null).lastName("Schwester Hanni").provisional(false).build();
|
||||
when(personRepository.findBySourceRef("schwester-hanni")).thenReturn(Optional.of(confirmed));
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("schwester-hanni").lastName("Schwester Hanni")
|
||||
.personType(PersonType.PERSON).provisional(true).build();
|
||||
|
||||
Person result = personService.upsertBySourceRef(cmd);
|
||||
|
||||
assertThat(result.isProvisional()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void upsertBySourceRef_setsProvisionalTrue_forNewProvisionalPerson() {
|
||||
when(personRepository.findBySourceRef("noise-geschirr")).thenReturn(Optional.empty());
|
||||
when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
|
||||
|
||||
PersonUpsertCommand cmd = PersonUpsertCommand.builder()
|
||||
.sourceRef("noise-geschirr").lastName("Tante Tüten")
|
||||
.personType(PersonType.PERSON).provisional(true).build();
|
||||
|
||||
Person result = personService.upsertBySourceRef(cmd);
|
||||
|
||||
assertThat(result.isProvisional()).isTrue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user