From 1558881c01d199f2d51b5c095dfb314651e8b9b9 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 14 Apr 2026 10:39:50 +0200 Subject: [PATCH] feat(annotations): add updateAnnotation service method with partial-update DTO Co-Authored-By: Claude Sonnet 4.6 --- .../dto/UpdateAnnotationDTO.java | 29 +++++++++++++++ .../service/AnnotationService.java | 16 ++++++++ .../service/AnnotationServiceTest.java | 37 +++++++++++++++++++ 3 files changed, 82 insertions(+) create mode 100644 backend/src/main/java/org/raddatz/familienarchiv/dto/UpdateAnnotationDTO.java diff --git a/backend/src/main/java/org/raddatz/familienarchiv/dto/UpdateAnnotationDTO.java b/backend/src/main/java/org/raddatz/familienarchiv/dto/UpdateAnnotationDTO.java new file mode 100644 index 00000000..19fa3c7d --- /dev/null +++ b/backend/src/main/java/org/raddatz/familienarchiv/dto/UpdateAnnotationDTO.java @@ -0,0 +1,29 @@ +package org.raddatz.familienarchiv.dto; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.AllArgsConstructor; + +/** + * Partial update payload for annotation position and size. + * All fields are optional — only non-null values are applied. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UpdateAnnotationDTO { + + @DecimalMin("0.0") @DecimalMax("1.0") + private Double x; + + @DecimalMin("0.0") @DecimalMax("1.0") + private Double y; + + @DecimalMin("0.01") @DecimalMax("1.0") + private Double width; + + @DecimalMin("0.01") @DecimalMax("1.0") + private Double height; +} diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java index 7b3e1252..e43b78b5 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java @@ -2,6 +2,7 @@ package org.raddatz.familienarchiv.service; import lombok.RequiredArgsConstructor; import org.raddatz.familienarchiv.dto.CreateAnnotationDTO; +import org.raddatz.familienarchiv.dto.UpdateAnnotationDTO; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.model.DocumentAnnotation; @@ -61,6 +62,21 @@ public class AnnotationService { return annotationRepository.save(annotation); } + @Transactional + public DocumentAnnotation updateAnnotation(UUID documentId, UUID annotationId, UpdateAnnotationDTO dto) { + DocumentAnnotation annotation = annotationRepository + .findByIdAndDocumentId(annotationId, documentId) + .orElseThrow(() -> DomainException.notFound( + ErrorCode.ANNOTATION_NOT_FOUND, "Annotation not found: " + annotationId)); + + if (dto.getX() != null) annotation.setX(dto.getX()); + if (dto.getY() != null) annotation.setY(dto.getY()); + if (dto.getWidth() != null) annotation.setWidth(dto.getWidth()); + if (dto.getHeight() != null) annotation.setHeight(dto.getHeight()); + + return annotationRepository.save(annotation); + } + @Transactional public void deleteAnnotation(UUID documentId, UUID annotationId, UUID userId) { DocumentAnnotation annotation = annotationRepository diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java index cd736005..549edd00 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java @@ -6,6 +6,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.raddatz.familienarchiv.dto.CreateAnnotationDTO; +import org.raddatz.familienarchiv.dto.UpdateAnnotationDTO; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.model.DocumentAnnotation; import org.raddatz.familienarchiv.repository.AnnotationRepository; @@ -203,6 +204,42 @@ class AnnotationServiceTest { .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(FORBIDDEN)); } + // ─── updateAnnotation ───────────────────────────────────────────────────── + + @Test + void updateAnnotation_throwsNotFound_whenAnnotationNotInDocument() { + UUID docId = UUID.randomUUID(); + UUID annotId = UUID.randomUUID(); + when(annotationRepository.findByIdAndDocumentId(annotId, docId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> annotationService.updateAnnotation(docId, annotId, new UpdateAnnotationDTO())) + .isInstanceOf(DomainException.class) + .satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(NOT_FOUND)); + } + + @Test + void updateAnnotation_updatesOnlyPresentFields() { + UUID docId = UUID.randomUUID(); + UUID annotId = UUID.randomUUID(); + DocumentAnnotation annotation = DocumentAnnotation.builder() + .id(annotId).documentId(docId) + .x(0.1).y(0.2).width(0.3).height(0.4).build(); + when(annotationRepository.findByIdAndDocumentId(annotId, docId)) + .thenReturn(Optional.of(annotation)); + when(annotationRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + UpdateAnnotationDTO dto = new UpdateAnnotationDTO(); + dto.setX(0.5); + dto.setY(0.6); + + DocumentAnnotation result = annotationService.updateAnnotation(docId, annotId, dto); + + assertThat(result.getX()).isEqualTo(0.5); + assertThat(result.getY()).isEqualTo(0.6); + assertThat(result.getWidth()).isEqualTo(0.3); // unchanged + assertThat(result.getHeight()).isEqualTo(0.4); // unchanged + } + // ─── listAnnotations ────────────────────────────────────────────────────── @Test