From bc397048b7e1eed54e5f0d5f69b09cd69287e60f Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 6 Apr 2026 14:15:03 +0200 Subject: [PATCH] fix(search): use in-memory sort for SENDER to include documents with null sender INNER JOIN from Sort.by("sender.lastName") was excluding docs without a sender. Co-Authored-By: Claude Sonnet 4.6 --- .../service/DocumentService.java | 22 ++++++++++++++++++- .../service/DocumentServiceTest.java | 20 +++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) 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 3f15cc03..88235452 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/DocumentService.java @@ -291,11 +291,15 @@ public class DocumentService { .and(hasTagPartial(tagQ)) .and(hasStatus(status)); - Sort springSort = resolveSort(sort, dir); if (sort == DocumentSort.RECEIVER) { List results = documentRepository.findAll(spec); return sortByFirstReceiver(results, dir); } + if (sort == DocumentSort.SENDER) { + List results = documentRepository.findAll(spec); + return sortBySender(results, dir); + } + Sort springSort = resolveSort(sort, dir); return documentRepository.findAll(spec, springSort); } @@ -312,6 +316,22 @@ public class DocumentService { }; } + private List sortBySender(List documents, String dir) { + boolean ascending = "ASC".equalsIgnoreCase(dir); + Comparator nullSafeComparator = (a, b) -> { + if (a.isEmpty() && b.isEmpty()) return 0; + if (a.isEmpty()) return ascending ? 1 : -1; + if (b.isEmpty()) return ascending ? -1 : 1; + return ascending ? a.compareTo(b) : b.compareTo(a); + }; + return documents.stream() + .sorted(Comparator.comparing(doc -> { + Person s = doc.getSender(); + return s != null ? s.getLastName() + " " + s.getFirstName() : ""; + }, nullSafeComparator)) + .toList(); + } + private List sortByFirstReceiver(List documents, String dir) { boolean ascending = "ASC".equalsIgnoreCase(dir); Comparator nullSafeComparator = (a, b) -> { 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 5a11ba6d..38a369ce 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/DocumentServiceTest.java @@ -10,6 +10,7 @@ import org.raddatz.familienarchiv.dto.DocumentUpdateDTO; import org.raddatz.familienarchiv.dto.IncompleteDocumentDTO; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.model.Document; +import org.raddatz.familienarchiv.model.DocumentSort; import org.raddatz.familienarchiv.model.DocumentStatus; import org.raddatz.familienarchiv.model.Person; import org.raddatz.familienarchiv.model.Tag; @@ -1273,4 +1274,23 @@ class DocumentServiceTest { verify(documentRepository).findConversation(eq(senderId), eq(receiverId), any(), any(), eq(sort)); verify(documentRepository, never()).findSinglePersonCorrespondence(any(), any(), any(), any()); } + + // ─── searchDocuments — SENDER sort includes documents with null sender ───── + + @Test + void searchDocuments_senderSort_includesDocumentsWithNullSender() { + Person alice = Person.builder().id(UUID.randomUUID()).firstName("Alice").lastName("Ziegler").build(); + Document withSender = Document.builder().id(UUID.randomUUID()).title("Has Sender").sender(alice).build(); + Document noSender = Document.builder().id(UUID.randomUUID()).title("No Sender").build(); + + // The repository returns both documents (no filtering by sender) + when(documentRepository.findAll(any(org.springframework.data.jpa.domain.Specification.class))) + .thenReturn(List.of(withSender, noSender)); + + List result = documentService.searchDocuments( + null, null, null, null, null, null, null, null, DocumentSort.SENDER, "asc"); + + assertThat(result).hasSize(2); + assertThat(result).extracting(Document::getTitle).containsExactly("Has Sender", "No Sender"); + } }