diff --git a/backend/src/main/java/org/raddatz/familienarchiv/importing/DocumentImporter.java b/backend/src/main/java/org/raddatz/familienarchiv/importing/DocumentImporter.java index 52465413..6be566ab 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/importing/DocumentImporter.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/importing/DocumentImporter.java @@ -165,6 +165,10 @@ public class DocumentImporter { doc.setContentType(contentType); doc.setSender(sender); doc.setSenderText(blankToNull(senderName)); + // The canonical row is authoritative for receivers/tags (ADR-025): clear then + // re-populate so a shrunk set on re-import prunes stale links rather than + // accumulating them. The raw sender_text/receiver_text retention is separate. + doc.getReceivers().clear(); doc.getReceivers().addAll(receivers); doc.setReceiverText(blankToNull(receiverNames)); doc.setDocumentDate(parseIsoDate(row.get("date_iso"))); @@ -203,7 +207,10 @@ public class DocumentImporter { .build())); } + // Authoritative: the canonical row defines the document's tags exactly. Clearing first + // means a tag removed from the row is pruned on re-import (ADR-025). private void attachTag(Document doc, String tagPath) { + doc.getTags().clear(); if (tagPath.isBlank()) return; tagService.findBySourceRef(tagPath).ifPresent(tag -> doc.getTags().add(tag)); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/importing/DocumentImporterTest.java b/backend/src/test/java/org/raddatz/familienarchiv/importing/DocumentImporterTest.java index bdcaa76b..f0b2263b 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/importing/DocumentImporterTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/importing/DocumentImporterTest.java @@ -382,6 +382,28 @@ class DocumentImporterTest { verify(documentService).save(org.mockito.ArgumentMatchers.argThat(d -> d.getId().equals(existing.getId()))); } + // ─── canonical collections are authoritative — re-import prunes removed links ────── + + @Test + void load_prunesReceiversAndTags_whenCanonicalRowShrinks(@TempDir Path tempDir) throws Exception { + ReflectionTestUtils.setField(importer, "importDir", tempDir.toString()); + Person staleReceiver = Person.builder().id(UUID.randomUUID()).sourceRef("stale-receiver").lastName("Stale").build(); + Tag staleTag = Tag.builder().id(UUID.randomUUID()).name("Stale").sourceRef("Themen/Stale").build(); + Document existing = Document.builder().id(UUID.randomUUID()) + .originalFilename("W-0008").status(DocumentStatus.PLACEHOLDER).build(); + existing.getReceivers().add(staleReceiver); + existing.getTags().add(staleTag); + when(documentService.findByOriginalFilename("W-0008")).thenReturn(Optional.of(existing)); + when(documentService.save(any())).thenAnswer(inv -> inv.getArgument(0)); + // The canonical row now carries no receiver and no tag: both stale links must go. + Path xlsx = writeDocs(tempDir, docRow("W-0008", "", "", "", "", "", "", "", "", "")); + + importer.load(xlsx.toFile()); + + verify(documentService).save(org.mockito.ArgumentMatchers.argThat(d -> + d.getReceivers().isEmpty() && d.getTags().isEmpty())); + } + // ─── helpers ───────────────────────────────────────────────────────────────────── private Map docRow(String index, String file, String senderId, String senderName,