diff --git a/backend/src/main/java/org/raddatz/familienarchiv/tag/TagRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/tag/TagRepository.java index 4a7fab90..f1b3b7ab 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/tag/TagRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/tag/TagRepository.java @@ -22,6 +22,9 @@ public interface TagRepository extends JpaRepository { Optional findByNameIgnoreCase(String name); + // Lookup by the canonical tag_path, used for idempotent canonical re-import (Phase 3). + Optional findBySourceRef(String sourceRef); + List findByNameContainingIgnoreCase(String name); /** diff --git a/backend/src/main/java/org/raddatz/familienarchiv/tag/TagService.java b/backend/src/main/java/org/raddatz/familienarchiv/tag/TagService.java index a572f84f..46a25712 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/tag/TagService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/tag/TagService.java @@ -55,6 +55,26 @@ public class TagService { .orElseGet(() -> tagRepository.save(Tag.builder().name(cleanName).build())); } + /** + * Idempotent upsert keyed on {@code sourceRef} (the canonical tag_path) for the + * Phase-3 importer (ADR-025). On first import the canonical name and parent are + * written; on re-import a human-renamed tag name is preserved (the source_ref is the + * stable identity, the name is a human-editable label). + */ + @Transactional + public Tag upsertBySourceRef(String sourceRef, String name, UUID parentId) { + return tagRepository.findBySourceRef(sourceRef) + .map(existing -> { + existing.setParentId(parentId); + return tagRepository.save(existing); + }) + .orElseGet(() -> tagRepository.save(Tag.builder() + .sourceRef(sourceRef) + .name(name) + .parentId(parentId) + .build())); + } + @Transactional public Tag update(UUID id, TagUpdateDTO dto) { Tag tag = getById(id); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/tag/TagImportUpsertTest.java b/backend/src/test/java/org/raddatz/familienarchiv/tag/TagImportUpsertTest.java new file mode 100644 index 00000000..c2e29dc0 --- /dev/null +++ b/backend/src/test/java/org/raddatz/familienarchiv/tag/TagImportUpsertTest.java @@ -0,0 +1,62 @@ +package org.raddatz.familienarchiv.tag; + +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 TagImportUpsertTest { + + @Mock TagRepository tagRepository; + @InjectMocks TagService tagService; + + @Test + void upsertBySourceRef_insertsNewTag_whenSourceRefUnknown() { + when(tagRepository.findBySourceRef("Themen/Brautbriefe")).thenReturn(Optional.empty()); + when(tagRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + UUID parentId = UUID.randomUUID(); + Tag result = tagService.upsertBySourceRef("Themen/Brautbriefe", "Brautbriefe", parentId); + + assertThat(result.getSourceRef()).isEqualTo("Themen/Brautbriefe"); + assertThat(result.getName()).isEqualTo("Brautbriefe"); + assertThat(result.getParentId()).isEqualTo(parentId); + } + + @Test + void upsertBySourceRef_updatesInPlace_whenSourceRefExists() { + Tag existing = Tag.builder().id(UUID.randomUUID()).name("Brautbriefe") + .sourceRef("Themen/Brautbriefe").build(); + when(tagRepository.findBySourceRef("Themen/Brautbriefe")).thenReturn(Optional.of(existing)); + when(tagRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + tagService.upsertBySourceRef("Themen/Brautbriefe", "Brautbriefe", null); + + verify(tagRepository).save(argThat(t -> t.getId().equals(existing.getId()))); + verify(tagRepository, never()).save(argThat(t -> t.getId() == null)); + } + + @Test + void upsertBySourceRef_preservesHumanRenamedTag_onReimport() { + Tag humanRenamed = Tag.builder().id(UUID.randomUUID()).name("Verlobungsbriefe") + .sourceRef("Themen/Brautbriefe").build(); + when(tagRepository.findBySourceRef("Themen/Brautbriefe")).thenReturn(Optional.of(humanRenamed)); + when(tagRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + Tag result = tagService.upsertBySourceRef("Themen/Brautbriefe", "Brautbriefe", null); + + assertThat(result.getName()).isEqualTo("Verlobungsbriefe"); + } +}