diff --git a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java index 175ab529..2dcdf777 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/person/PersonService.java @@ -177,6 +177,7 @@ public class PersonService { .notes(blankToNull(cmd.notes())) .birthYear(cmd.birthYear()) .deathYear(cmd.deathYear()) + .generation(cmd.generation()) .familyMember(cmd.familyMember()) .personType(cmd.personType() == null ? PersonType.PERSON : cmd.personType()) .provisional(cmd.provisional()) @@ -200,6 +201,7 @@ public class PersonService { existing.setNotes(preferHuman(existing.getNotes(), cmd.notes())); existing.setBirthYear(preferHuman(existing.getBirthYear(), cmd.birthYear())); existing.setDeathYear(preferHuman(existing.getDeathYear(), cmd.deathYear())); + existing.setGeneration(preferHuman(existing.getGeneration(), cmd.generation())); if (cmd.personType() != null && existing.getPersonType() == PersonType.PERSON) { existing.setPersonType(cmd.personType()); } @@ -254,6 +256,7 @@ public class PersonService { .notes(dto.getNotes() == null || dto.getNotes().isBlank() ? null : dto.getNotes().trim()) .birthYear(dto.getBirthYear()) .deathYear(dto.getDeathYear()) + .generation(dto.getGeneration()) .build(); return personRepository.save(person); } @@ -286,6 +289,9 @@ public class PersonService { person.setNotes(dto.getNotes() == null || dto.getNotes().isBlank() ? null : dto.getNotes().trim()); person.setBirthYear(dto.getBirthYear()); person.setDeathYear(dto.getDeathYear()); + // Form path: a human can clear generation back to null. Unlike the importer + // which routes through preferHuman, we write the DTO value verbatim. + person.setGeneration(dto.getGeneration()); return personRepository.save(person); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonImportUpsertTest.java b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonImportUpsertTest.java index c8b81b2b..9b778292 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonImportUpsertTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonImportUpsertTest.java @@ -148,4 +148,55 @@ class PersonImportUpsertTest { assertThat(result.isProvisional()).isTrue(); } + + // ─── generation (#689) ───────────────────────────────────────────────────── + + @Test + void upsertBySourceRef_writesGeneration_onFirstImport() { + when(personRepository.findBySourceRef("herbert-cram")).thenReturn(Optional.empty()); + when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + PersonUpsertCommand cmd = PersonUpsertCommand.builder() + .sourceRef("herbert-cram").firstName("Herbert").lastName("Cram") + .generation(3).personType(PersonType.PERSON).provisional(false).build(); + + Person result = personService.upsertBySourceRef(cmd); + + assertThat(result.getGeneration()).isEqualTo(3); + } + + @Test + void upsertBySourceRef_preservesHumanEditedGeneration_onReimport() { + Person humanEdited = Person.builder() + .id(UUID.randomUUID()).sourceRef("herbert-cram") + .firstName("Herbert").lastName("Cram").generation(4).build(); + when(personRepository.findBySourceRef("herbert-cram")).thenReturn(Optional.of(humanEdited)); + when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + PersonUpsertCommand cmd = PersonUpsertCommand.builder() + .sourceRef("herbert-cram").firstName("Herbert").lastName("Cram") + .generation(2).personType(PersonType.PERSON).provisional(false).build(); + + Person result = personService.upsertBySourceRef(cmd); + + assertThat(result.getGeneration()).isEqualTo(4); + } + + @Test + void mergeCanonical_overwrites_human_null_with_canonical_value_documenting_known_limitation() { + // If preferHuman gains explicit-null-vs-unset semantics, delete this test (see issue #689). + Person existing = Person.builder() + .id(UUID.randomUUID()).sourceRef("herbert-cram") + .firstName("Herbert").lastName("Cram").generation(null).build(); + when(personRepository.findBySourceRef("herbert-cram")).thenReturn(Optional.of(existing)); + when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + PersonUpsertCommand cmd = PersonUpsertCommand.builder() + .sourceRef("herbert-cram").firstName("Herbert").lastName("Cram") + .generation(3).personType(PersonType.PERSON).provisional(false).build(); + + Person result = personService.upsertBySourceRef(cmd); + + assertThat(result.getGeneration()).isEqualTo(3); + } } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceTest.java index 4c8de65c..a42a3051 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/person/PersonServiceTest.java @@ -261,6 +261,54 @@ class PersonServiceTest { .isEqualTo(400); } + @Test + void createPerson_dto_persistsGeneration() { + when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + PersonUpdateDTO dto = new PersonUpdateDTO(); + dto.setFirstName("Hans"); dto.setLastName("Raddatz"); + dto.setPersonType(PersonType.PERSON); dto.setGeneration(3); + + Person result = personService.createPerson(dto); + + assertThat(result.getGeneration()).isEqualTo(3); + } + + @Test + void updatePerson_writesGeneration_includingExplicitNullClear() { + // The form path is the only place a human can clear generation back to null. + UUID id = UUID.randomUUID(); + Person existing = Person.builder().id(id).firstName("Hans").lastName("Raddatz") + .personType(PersonType.PERSON).generation(3).build(); + when(personRepository.findById(id)).thenReturn(Optional.of(existing)); + when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + PersonUpdateDTO dto = new PersonUpdateDTO(); + dto.setFirstName("Hans"); dto.setLastName("Raddatz"); + dto.setPersonType(PersonType.PERSON); dto.setGeneration(null); + + Person result = personService.updatePerson(id, dto); + + assertThat(result.getGeneration()).isNull(); + } + + @Test + void updatePerson_writesGeneration_whenSet() { + UUID id = UUID.randomUUID(); + Person existing = Person.builder().id(id).firstName("Hans").lastName("Raddatz") + .personType(PersonType.PERSON).build(); + when(personRepository.findById(id)).thenReturn(Optional.of(existing)); + when(personRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + PersonUpdateDTO dto = new PersonUpdateDTO(); + dto.setFirstName("Hans"); dto.setLastName("Raddatz"); + dto.setPersonType(PersonType.PERSON); dto.setGeneration(2); + + Person result = personService.updatePerson(id, dto); + + assertThat(result.getGeneration()).isEqualTo(2); + } + // ─── updatePerson (personType) ─────────────────────────────────────────── @Test