feat(bulk-edit): add DocumentService.applyBulkEditToDocument
Per-document atomic mutation method for the upcoming bulk PATCH endpoint. Tags and receivers merge additively into existing sets; sender and the three location fields replace only when the DTO field is non-blank. Wrapped in its own @Transactional so a per-document failure cannot partially mutate other documents in the outer batch loop. Refs #225 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -350,6 +350,52 @@ public class DocumentService {
|
|||||||
return documentRepository.save(doc);
|
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<Tag> 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<Person> 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)
|
* Hilfsmethode: Erstellt Platzhalter (wird später vom Excel-Service genutzt)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1917,4 +1917,198 @@ class DocumentServiceTest {
|
|||||||
.isInstanceOf(DomainException.class)
|
.isInstanceOf(DomainException.class)
|
||||||
.hasMessageContaining("titles");
|
.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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user