diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java index 071f2276..1cfe26f6 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java @@ -57,6 +57,7 @@ public interface DocumentRepository extends JpaRepository, JpaSp @EntityGraph("Document.full") List findByReceiversId(UUID receiverId); + // Callers access only doc.getTags() to mutate the set — receivers/sender not touched; no graph needed. List findByTags_Id(UUID tagId); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java index faa74367..6ce9ba6e 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java @@ -1034,7 +1034,25 @@ public class DocumentService { } public DocumentSearchResult searchDocumentsByPersonId(UUID personId, LocalDate from, LocalDate to, Pageable pageable) { - throw new UnsupportedOperationException("Implemented in Task 12 — findBySenderOrReceiver JPQL"); + Person person = personService.getById(personId); + Specification spec = buildPersonSpec(person, from, to); + Page page = documentRepository.findAll(spec, pageable); + List items = enrichItems(page.getContent(), null); + return DocumentSearchResult.paged(items, pageable, page.getTotalElements()); + } + + private Specification buildPersonSpec(Person person, LocalDate from, LocalDate to) { + return (root, query, cb) -> { + if (query != null) query.distinct(true); + var receiversJoin = root.join("receivers", jakarta.persistence.criteria.JoinType.LEFT); + var senderPredicate = cb.equal(root.get("sender"), person); + var receiverPredicate = cb.equal(receiversJoin, person); + var personPredicate = cb.or(senderPredicate, receiverPredicate); + var predicates = new java.util.ArrayList<>(java.util.List.of(personPredicate)); + if (from != null) predicates.add(cb.greaterThanOrEqualTo(root.get("documentDate"), from)); + if (to != null) predicates.add(cb.lessThanOrEqualTo(root.get("documentDate"), to)); + return cb.and(predicates.toArray(new jakarta.persistence.criteria.Predicate[0])); + }; } public long getIncompleteCount() { diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentRepositoryTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentRepositoryTest.java index 234abbd8..eff82466 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentRepositoryTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentRepositoryTest.java @@ -624,4 +624,88 @@ class DocumentRepositoryTest { .reviewed(reviewed) .build(); } + + // ─── searchDocumentsByPersonId (via Specification) ─────────────────────── + + private Page searchByPerson(Person person, LocalDate from, LocalDate to) { + Specification spec = (root, query, cb) -> { + if (query != null) query.distinct(true); + var receiversJoin = root.join("receivers", jakarta.persistence.criteria.JoinType.LEFT); + var personPredicate = cb.or( + cb.equal(root.get("sender"), person), + cb.equal(receiversJoin, person)); + var predicates = new java.util.ArrayList<>(java.util.List.of(personPredicate)); + if (from != null) predicates.add(cb.greaterThanOrEqualTo(root.get("documentDate"), from)); + if (to != null) predicates.add(cb.lessThanOrEqualTo(root.get("documentDate"), to)); + return cb.and(predicates.toArray(new jakarta.persistence.criteria.Predicate[0])); + }; + return documentRepository.findAll(spec, PageRequest.of(0, 10)); + } + + @Test + void searchByPersonSpec_returnsDocument_whenPersonIsSender() { + Person person = personRepository.save(Person.builder().lastName("Raddatz").build()); + Document doc = documentRepository.save(Document.builder() + .title("Senderbrief").originalFilename("sender.pdf") + .status(DocumentStatus.UPLOADED).sender(person).build()); + + Page result = searchByPerson(person, null, null); + + assertThat(result.getContent()).extracting(Document::getId).containsExactly(doc.getId()); + } + + @Test + void searchByPersonSpec_returnsDocument_whenPersonIsReceiver() { + Person person = personRepository.save(Person.builder().lastName("Raddatz").build()); + Document doc = documentRepository.save(Document.builder() + .title("Empfängerbrief").originalFilename("receiver.pdf") + .status(DocumentStatus.UPLOADED) + .receivers(new java.util.HashSet<>(List.of(person))).build()); + + Page result = searchByPerson(person, null, null); + + assertThat(result.getContent()).extracting(Document::getId).containsExactly(doc.getId()); + } + + @Test + void searchByPersonSpec_returnsDocumentOnce_whenPersonIsBothSenderAndReceiver() { + Person person = personRepository.save(Person.builder().lastName("Raddatz").build()); + Document doc = documentRepository.save(Document.builder() + .title("SenderEmpfänger").originalFilename("both.pdf") + .status(DocumentStatus.UPLOADED).sender(person) + .receivers(new java.util.HashSet<>(List.of(person))).build()); + + Page result = searchByPerson(person, null, null); + + assertThat(result.getContent()).hasSize(1); + assertThat(result.getContent().get(0).getId()).isEqualTo(doc.getId()); + } + + @Test + void searchByPersonSpec_excludesDocuments_outsideDateRange() { + Person person = personRepository.save(Person.builder().lastName("Raddatz").build()); + Document inside = documentRepository.save(Document.builder() + .title("Innen").originalFilename("inside.pdf").status(DocumentStatus.UPLOADED) + .sender(person).documentDate(LocalDate.of(1918, 6, 15)).build()); + documentRepository.save(Document.builder() + .title("Außen").originalFilename("outside.pdf").status(DocumentStatus.UPLOADED) + .sender(person).documentDate(LocalDate.of(1920, 1, 1)).build()); + + Page result = searchByPerson(person, LocalDate.of(1914, 1, 1), LocalDate.of(1918, 12, 31)); + + assertThat(result.getContent()).extracting(Document::getId).containsExactly(inside.getId()); + } + + @Test + void searchByPersonSpec_returnsEmpty_whenNoMatchingDocuments() { + Person person = personRepository.save(Person.builder().lastName("Raddatz").build()); + Person other = personRepository.save(Person.builder().lastName("Braun").build()); + documentRepository.save(Document.builder() + .title("Fremder Brief").originalFilename("other.pdf") + .status(DocumentStatus.UPLOADED).sender(other).build()); + + Page result = searchByPerson(person, null, null); + + assertThat(result.getContent()).isEmpty(); + } }