fix(importing): make document receivers/tags canonical-authoritative on re-import

The DocumentImporter accumulated receivers/tags via addAll without pruning, so a
shrunk canonical row left stale links on a re-imported PLACEHOLDER document. Clear
the collections before re-populating so the canonical row is authoritative: a removed
receiver/tag is now pruned. Raw sender_text/receiver_text retention is unchanged.

Refs #669

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-27 10:58:57 +02:00
parent 5cf8fd149e
commit 2f7ea37466
2 changed files with 29 additions and 0 deletions

View File

@@ -165,6 +165,10 @@ public class DocumentImporter {
doc.setContentType(contentType); doc.setContentType(contentType);
doc.setSender(sender); doc.setSender(sender);
doc.setSenderText(blankToNull(senderName)); 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.getReceivers().addAll(receivers);
doc.setReceiverText(blankToNull(receiverNames)); doc.setReceiverText(blankToNull(receiverNames));
doc.setDocumentDate(parseIsoDate(row.get("date_iso"))); doc.setDocumentDate(parseIsoDate(row.get("date_iso")));
@@ -203,7 +207,10 @@ public class DocumentImporter {
.build())); .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) { private void attachTag(Document doc, String tagPath) {
doc.getTags().clear();
if (tagPath.isBlank()) return; if (tagPath.isBlank()) return;
tagService.findBySourceRef(tagPath).ifPresent(tag -> doc.getTags().add(tag)); tagService.findBySourceRef(tagPath).ifPresent(tag -> doc.getTags().add(tag));
} }

View File

@@ -382,6 +382,28 @@ class DocumentImporterTest {
verify(documentService).save(org.mockito.ArgumentMatchers.argThat(d -> d.getId().equals(existing.getId()))); 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 ───────────────────────────────────────────────────────────────────── // ─── helpers ─────────────────────────────────────────────────────────────────────
private Map<String, String> docRow(String index, String file, String senderId, String senderName, private Map<String, String> docRow(String index, String file, String senderId, String senderName,