diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java index b7d50f99..f53a60b3 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -350,6 +350,52 @@ public class DocumentService { return documentRepository.save(doc); } + /** + * Applies a bulk-edit DTO to a single document atomically. + * Tags and receivers are additive (merged into existing sets); sender and the + * three location fields are replace-on-non-blank (null/blank means "no change"). + * Wrapped in its own transaction so a failure on one document never partially + * mutates another in the batch loop. + */ + @Transactional + public Document applyBulkEditToDocument(UUID id, org.raddatz.familienarchiv.dto.DocumentBulkEditDTO dto) { + Document doc = documentRepository.findById(id) + .orElseThrow(() -> DomainException.notFound(ErrorCode.DOCUMENT_NOT_FOUND, "Document not found: " + id)); + + if (dto.getTagNames() != null && !dto.getTagNames().isEmpty()) { + Set merged = new HashSet<>(doc.getTags()); + for (String name : dto.getTagNames()) { + String clean = name.trim(); + if (!clean.isEmpty()) { + merged.add(tagService.findOrCreate(clean)); + } + } + doc.setTags(merged); + } + + if (dto.getSenderId() != null) { + doc.setSender(personService.getById(dto.getSenderId())); + } + + if (dto.getReceiverIds() != null && !dto.getReceiverIds().isEmpty()) { + Set merged = new HashSet<>(doc.getReceivers()); + merged.addAll(personService.getAllById(dto.getReceiverIds())); + doc.setReceivers(merged); + } + + if (StringUtils.hasText(dto.getDocumentLocation())) { + doc.setDocumentLocation(dto.getDocumentLocation()); + } + if (StringUtils.hasText(dto.getArchiveBox())) { + doc.setArchiveBox(dto.getArchiveBox()); + } + if (StringUtils.hasText(dto.getArchiveFolder())) { + doc.setArchiveFolder(dto.getArchiveFolder()); + } + + return documentRepository.save(doc); + } + /** * Hilfsmethode: Erstellt Platzhalter (wird später vom Excel-Service genutzt) */ diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java index 4c34862b..3065391c 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java @@ -1917,4 +1917,198 @@ class DocumentServiceTest { .isInstanceOf(DomainException.class) .hasMessageContaining("titles"); } + + // ─── applyBulkEditToDocument ───────────────────────────────────────────── + + private static org.raddatz.familienarchiv.dto.DocumentBulkEditDTO bulkDto() { + return new org.raddatz.familienarchiv.dto.DocumentBulkEditDTO(); + } + + @Test + void applyBulkEditToDocument_throwsNotFound_whenDocumentMissing() { + UUID id = UUID.randomUUID(); + when(documentRepository.findById(id)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> documentService.applyBulkEditToDocument(id, bulkDto())) + .isInstanceOf(DomainException.class) + .hasMessageContaining(id.toString()); + } + + @Test + void applyBulkEditToDocument_appliesTagsAdditively_preservesExistingTags() { + UUID id = UUID.randomUUID(); + Tag existing = Tag.builder().id(UUID.randomUUID()).name("Brief").build(); + Tag added = Tag.builder().id(UUID.randomUUID()).name("Kurrent").build(); + Document doc = Document.builder().id(id).title("T") + .tags(new HashSet<>(Set.of(existing))) + .receivers(new HashSet<>()) + .build(); + when(documentRepository.findById(id)).thenReturn(Optional.of(doc)); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + when(tagService.findOrCreate("Kurrent")).thenReturn(added); + + var dto = bulkDto(); + dto.setTagNames(List.of("Kurrent")); + documentService.applyBulkEditToDocument(id, dto); + + assertThat(doc.getTags()).containsExactlyInAnyOrder(existing, added); + } + + @Test + void applyBulkEditToDocument_skipsTags_whenTagNamesIsNull() { + UUID id = UUID.randomUUID(); + Tag existing = Tag.builder().id(UUID.randomUUID()).name("Brief").build(); + Document doc = Document.builder().id(id).title("T") + .tags(new HashSet<>(Set.of(existing))) + .receivers(new HashSet<>()) + .build(); + when(documentRepository.findById(id)).thenReturn(Optional.of(doc)); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + documentService.applyBulkEditToDocument(id, bulkDto()); + + assertThat(doc.getTags()).containsExactly(existing); + verify(tagService, never()).findOrCreate(any()); + } + + @Test + void applyBulkEditToDocument_skipsTags_whenTagNamesIsEmpty() { + UUID id = UUID.randomUUID(); + Tag existing = Tag.builder().id(UUID.randomUUID()).name("Brief").build(); + Document doc = Document.builder().id(id).title("T") + .tags(new HashSet<>(Set.of(existing))) + .receivers(new HashSet<>()) + .build(); + when(documentRepository.findById(id)).thenReturn(Optional.of(doc)); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + var dto = bulkDto(); + dto.setTagNames(List.of()); + documentService.applyBulkEditToDocument(id, dto); + + assertThat(doc.getTags()).containsExactly(existing); + verify(tagService, never()).findOrCreate(any()); + } + + @Test + void applyBulkEditToDocument_replacesSender_whenSenderIdProvided() { + UUID id = UUID.randomUUID(); + UUID senderId = UUID.randomUUID(); + Person oldSender = Person.builder().id(UUID.randomUUID()).firstName("Old").build(); + Person newSender = Person.builder().id(senderId).firstName("New").build(); + Document doc = Document.builder().id(id).title("T") + .sender(oldSender) + .receivers(new HashSet<>()) + .build(); + when(documentRepository.findById(id)).thenReturn(Optional.of(doc)); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + when(personService.getById(senderId)).thenReturn(newSender); + + var dto = bulkDto(); + dto.setSenderId(senderId); + documentService.applyBulkEditToDocument(id, dto); + + assertThat(doc.getSender()).isEqualTo(newSender); + } + + @Test + void applyBulkEditToDocument_skipsSender_whenSenderIdIsNull() { + UUID id = UUID.randomUUID(); + Person existing = Person.builder().id(UUID.randomUUID()).firstName("X").build(); + Document doc = Document.builder().id(id).title("T") + .sender(existing) + .receivers(new HashSet<>()) + .build(); + when(documentRepository.findById(id)).thenReturn(Optional.of(doc)); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + documentService.applyBulkEditToDocument(id, bulkDto()); + + assertThat(doc.getSender()).isEqualTo(existing); + verify(personService, never()).getById(any()); + } + + @Test + void applyBulkEditToDocument_addsReceiversAdditively_preservesExistingReceivers() { + UUID id = UUID.randomUUID(); + UUID newReceiverId = UUID.randomUUID(); + Person existing = Person.builder().id(UUID.randomUUID()).firstName("Old").build(); + Person added = Person.builder().id(newReceiverId).firstName("New").build(); + Document doc = Document.builder().id(id).title("T") + .receivers(new HashSet<>(Set.of(existing))) + .build(); + when(documentRepository.findById(id)).thenReturn(Optional.of(doc)); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + when(personService.getAllById(List.of(newReceiverId))).thenReturn(List.of(added)); + + var dto = bulkDto(); + dto.setReceiverIds(List.of(newReceiverId)); + documentService.applyBulkEditToDocument(id, dto); + + assertThat(doc.getReceivers()).containsExactlyInAnyOrder(existing, added); + } + + @Test + void applyBulkEditToDocument_skipsReceivers_whenReceiverIdsIsNullOrEmpty() { + UUID id = UUID.randomUUID(); + Person existing = Person.builder().id(UUID.randomUUID()).firstName("Old").build(); + Document doc = Document.builder().id(id).title("T") + .receivers(new HashSet<>(Set.of(existing))) + .build(); + when(documentRepository.findById(id)).thenReturn(Optional.of(doc)); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + var dto = bulkDto(); + dto.setReceiverIds(List.of()); + documentService.applyBulkEditToDocument(id, dto); + + assertThat(doc.getReceivers()).containsExactly(existing); + verify(personService, never()).getAllById(any()); + } + + @Test + void applyBulkEditToDocument_replacesArchiveBoxAndFolderAndDocumentLocation_whenProvided() { + UUID id = UUID.randomUUID(); + Document doc = Document.builder().id(id).title("T") + .archiveBox("OldBox") + .archiveFolder("OldFolder") + .documentLocation("OldLocation") + .receivers(new HashSet<>()) + .build(); + when(documentRepository.findById(id)).thenReturn(Optional.of(doc)); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + var dto = bulkDto(); + dto.setArchiveBox("NewBox"); + dto.setArchiveFolder("NewFolder"); + dto.setDocumentLocation("NewLocation"); + documentService.applyBulkEditToDocument(id, dto); + + assertThat(doc.getArchiveBox()).isEqualTo("NewBox"); + assertThat(doc.getArchiveFolder()).isEqualTo("NewFolder"); + assertThat(doc.getDocumentLocation()).isEqualTo("NewLocation"); + } + + @Test + void applyBulkEditToDocument_skipsLocationFields_whenBlankOrNull() { + UUID id = UUID.randomUUID(); + Document doc = Document.builder().id(id).title("T") + .archiveBox("KeepBox") + .archiveFolder("KeepFolder") + .documentLocation("KeepLocation") + .receivers(new HashSet<>()) + .build(); + when(documentRepository.findById(id)).thenReturn(Optional.of(doc)); + when(documentRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + var dto = bulkDto(); + dto.setArchiveBox(" "); + dto.setArchiveFolder(""); + // documentLocation left null + documentService.applyBulkEditToDocument(id, dto); + + assertThat(doc.getArchiveBox()).isEqualTo("KeepBox"); + assertThat(doc.getArchiveFolder()).isEqualTo("KeepFolder"); + assertThat(doc.getDocumentLocation()).isEqualTo("KeepLocation"); + } }