diff --git a/backend/src/main/java/org/raddatz/familienarchiv/importing/PersonTreeImporter.java b/backend/src/main/java/org/raddatz/familienarchiv/importing/PersonTreeImporter.java index 26ae0dcd..56eab828 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/importing/PersonTreeImporter.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/importing/PersonTreeImporter.java @@ -79,12 +79,32 @@ public class PersonTreeImporter { .notes(blankToNull(text(node, "notes"))) .birthYear(intOrNull(node, "birthYear")) .deathYear(intOrNull(node, "deathYear")) + .generation(generationOrNull(node, personId)) .familyMember(node.path("familyMember").asBoolean(false)) .personType(PersonType.PERSON) .provisional(false) .build(); } + /** + * Returns the JSON {@code generation} value if present and in + * {@value #GENERATION_MIN}..{@value #GENERATION_MAX}; null otherwise. Out-of-range + * values log a WARN but never abort the batch — mirrors the register-importer + * skip-and-warn policy. + */ + private static Integer generationOrNull(JsonNode node, String personId) { + Integer raw = intOrNull(node, "generation"); + if (raw == null) return null; + if (raw < GENERATION_MIN || raw > GENERATION_MAX) { + log.warn("Skipping out-of-range generation '{}' for person {}", raw, personId); + return null; + } + return raw; + } + + private static final int GENERATION_MIN = 0; + private static final int GENERATION_MAX = 10; + private int createRelationships(JsonNode relationships, Map idByRowId) { int created = 0; for (JsonNode node : relationships) { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/importing/PersonTreeImporterTest.java b/backend/src/test/java/org/raddatz/familienarchiv/importing/PersonTreeImporterTest.java index ce90d260..d43191bb 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/importing/PersonTreeImporterTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/importing/PersonTreeImporterTest.java @@ -151,6 +151,65 @@ class PersonTreeImporterTest { verify(relationshipService, org.mockito.Mockito.never()).addRelationship(any(), any()); } + // ─── generation (#689) ───────────────────────────────────────────────────── + + @Test + void load_passesGenerationFromJson(@TempDir Path tempDir) throws Exception { + PersonService personService = mock(PersonService.class); + RelationshipService relationshipService = mock(RelationshipService.class); + when(personService.upsertBySourceRef(any())).thenAnswer(inv -> personOf(inv.getArgument(0))); + Path json = write(tempDir, """ + {"persons":[ + {"rowId":"row_a","lastName":"Cram","firstName":"Herbert","familyMember":true, + "personId":"herbert-cram","generation":3} + ],"relationships":[]} + """); + + new PersonTreeImporter(personService, relationshipService).load(json.toFile()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PersonUpsertCommand.class); + verify(personService).upsertBySourceRef(captor.capture()); + assertThat(captor.getValue().generation()).isEqualTo(3); + } + + @Test + void load_returnsNullGeneration_whenAbsentFromJson(@TempDir Path tempDir) throws Exception { + PersonService personService = mock(PersonService.class); + RelationshipService relationshipService = mock(RelationshipService.class); + when(personService.upsertBySourceRef(any())).thenAnswer(inv -> personOf(inv.getArgument(0))); + Path json = write(tempDir, """ + {"persons":[ + {"rowId":"row_a","lastName":"Cram","firstName":"Herbert","familyMember":true, + "personId":"herbert-cram"} + ],"relationships":[]} + """); + + new PersonTreeImporter(personService, relationshipService).load(json.toFile()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PersonUpsertCommand.class); + verify(personService).upsertBySourceRef(captor.capture()); + assertThat(captor.getValue().generation()).isNull(); + } + + @Test + void load_skipsOutOfRangeGeneration_logsWarn_neverAborts(@TempDir Path tempDir) throws Exception { + PersonService personService = mock(PersonService.class); + RelationshipService relationshipService = mock(RelationshipService.class); + when(personService.upsertBySourceRef(any())).thenAnswer(inv -> personOf(inv.getArgument(0))); + Path json = write(tempDir, """ + {"persons":[ + {"rowId":"row_a","lastName":"Cram","firstName":"Herbert","familyMember":true, + "personId":"herbert-cram","generation":99} + ],"relationships":[]} + """); + + new PersonTreeImporter(personService, relationshipService).load(json.toFile()); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PersonUpsertCommand.class); + verify(personService).upsertBySourceRef(captor.capture()); + assertThat(captor.getValue().generation()).isNull(); + } + private static Person personOf(PersonUpsertCommand cmd) { return Person.builder().id(UUID.randomUUID()).sourceRef(cmd.sourceRef()).lastName(cmd.lastName()).build(); }