diff --git a/src/main/java/com/olhar/olharapi/application/port/in/GetPhotosUseCase.java b/src/main/java/com/olhar/olharapi/application/port/in/GetPhotosUseCase.java new file mode 100644 index 0000000..f089e55 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/application/port/in/GetPhotosUseCase.java @@ -0,0 +1,9 @@ +package com.olhar.olharapi.application.port.in; + +import com.olhar.olharapi.domain.model.Photo; + +import java.util.List; + +public interface GetPhotosUseCase { + List getAll(); +} diff --git a/src/main/java/com/olhar/olharapi/application/port/in/UpdatePhotoUseCase.java b/src/main/java/com/olhar/olharapi/application/port/in/UpdatePhotoUseCase.java new file mode 100644 index 0000000..ae5c8a3 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/application/port/in/UpdatePhotoUseCase.java @@ -0,0 +1,21 @@ +package com.olhar.olharapi.application.port.in; + +import com.olhar.olharapi.domain.model.Photo; + +import java.util.List; +import java.util.UUID; + +public interface UpdatePhotoUseCase { + Photo update(UUID id, Command command); + + record Command( + String title, + String date, + String place, + List people, + String comment, + String status, + String flagReason, + String flagBy + ) {} +} diff --git a/src/main/java/com/olhar/olharapi/application/port/in/UploadPhotosUseCase.java b/src/main/java/com/olhar/olharapi/application/port/in/UploadPhotosUseCase.java new file mode 100644 index 0000000..5fc5d26 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/application/port/in/UploadPhotosUseCase.java @@ -0,0 +1,12 @@ +package com.olhar.olharapi.application.port.in; + +import com.olhar.olharapi.domain.model.Photo; + +import java.io.InputStream; +import java.util.List; + +public interface UploadPhotosUseCase { + List upload(List files, String uploadedBy); + + record FileEntry(String originalFilename, String contentType, InputStream inputStream) {} +} diff --git a/src/main/java/com/olhar/olharapi/application/port/out/PhotoRepository.java b/src/main/java/com/olhar/olharapi/application/port/out/PhotoRepository.java new file mode 100644 index 0000000..bcdb590 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/application/port/out/PhotoRepository.java @@ -0,0 +1,13 @@ +package com.olhar.olharapi.application.port.out; + +import com.olhar.olharapi.domain.model.Photo; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface PhotoRepository { + List findAll(); + Optional findById(UUID id); + Photo save(Photo photo); +} diff --git a/src/main/java/com/olhar/olharapi/application/port/out/PhotoStoragePort.java b/src/main/java/com/olhar/olharapi/application/port/out/PhotoStoragePort.java new file mode 100644 index 0000000..02e02d9 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/application/port/out/PhotoStoragePort.java @@ -0,0 +1,9 @@ +package com.olhar.olharapi.application.port.out; + +import java.io.InputStream; +import java.util.UUID; + +public interface PhotoStoragePort { + String store(UUID photoId, String filename, InputStream data); + InputStream load(UUID photoId); +} diff --git a/src/main/java/com/olhar/olharapi/application/usecase/GetPhotosService.java b/src/main/java/com/olhar/olharapi/application/usecase/GetPhotosService.java new file mode 100644 index 0000000..cc82aeb --- /dev/null +++ b/src/main/java/com/olhar/olharapi/application/usecase/GetPhotosService.java @@ -0,0 +1,21 @@ +package com.olhar.olharapi.application.usecase; + +import com.olhar.olharapi.application.port.in.GetPhotosUseCase; +import com.olhar.olharapi.application.port.out.PhotoRepository; +import com.olhar.olharapi.domain.model.Photo; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class GetPhotosService implements GetPhotosUseCase { + + private final PhotoRepository photoRepository; + + @Override + public List getAll() { + return photoRepository.findAll(); + } +} diff --git a/src/main/java/com/olhar/olharapi/application/usecase/UpdatePhotoService.java b/src/main/java/com/olhar/olharapi/application/usecase/UpdatePhotoService.java new file mode 100644 index 0000000..b90beea --- /dev/null +++ b/src/main/java/com/olhar/olharapi/application/usecase/UpdatePhotoService.java @@ -0,0 +1,53 @@ +package com.olhar.olharapi.application.usecase; + +import com.olhar.olharapi.application.port.in.UpdatePhotoUseCase; +import com.olhar.olharapi.application.port.out.PhotoRepository; +import com.olhar.olharapi.domain.exception.PhotoNotFoundException; +import com.olhar.olharapi.domain.model.Photo; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class UpdatePhotoService implements UpdatePhotoUseCase { + + private final PhotoRepository photoRepository; + + @Override + @Transactional + public Photo update(UUID id, Command command) { + Photo existing = photoRepository.findById(id) + .orElseThrow(() -> new PhotoNotFoundException(id)); + + Photo.Status newStatus = command.status() != null + ? Photo.Status.fromApiStatus(command.status()) + : existing.status(); + + Instant valDate = switch (newStatus) { + case OK -> existing.valDate() != null ? existing.valDate() : Instant.now(); + default -> null; + }; + + Photo updated = new Photo( + existing.id(), + command.title() != null ? command.title() : existing.title(), + command.date() != null ? command.date() : existing.date(), + command.place() != null ? command.place() : existing.place(), + command.people() != null ? command.people() : existing.people(), + command.comment() != null ? command.comment() : existing.comment(), + newStatus, + existing.by(), + valDate, + command.flagReason() != null ? command.flagReason() : existing.flagReason(), + command.flagBy() != null ? command.flagBy() : existing.flagBy(), + existing.imageUrl(), + existing.createdAt() + ); + + return photoRepository.save(updated); + } +} diff --git a/src/main/java/com/olhar/olharapi/application/usecase/UploadPhotosService.java b/src/main/java/com/olhar/olharapi/application/usecase/UploadPhotosService.java new file mode 100644 index 0000000..295041f --- /dev/null +++ b/src/main/java/com/olhar/olharapi/application/usecase/UploadPhotosService.java @@ -0,0 +1,57 @@ +package com.olhar.olharapi.application.usecase; + +import com.olhar.olharapi.application.port.in.UploadPhotosUseCase; +import com.olhar.olharapi.application.port.out.PhotoRepository; +import com.olhar.olharapi.application.port.out.PhotoStoragePort; +import com.olhar.olharapi.domain.model.Photo; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class UploadPhotosService implements UploadPhotosUseCase { + + private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ISO_LOCAL_DATE; + + private final PhotoRepository photoRepository; + private final PhotoStoragePort photoStorage; + + @Override + @Transactional + public List upload(List files, String uploadedBy) { + return files.stream() + .map(file -> importOne(file, uploadedBy)) + .toList(); + } + + private Photo importOne(FileEntry file, String uploadedBy) { + UUID id = UUID.randomUUID(); + String imageUrl = photoStorage.store(id, file.originalFilename(), file.inputStream()); + String today = LocalDate.now().format(DATE_FMT); + + Photo photo = new Photo( + id, + null, + today, + null, + List.of(), + null, + Photo.Status.TODO, + uploadedBy, + null, + null, + null, + imageUrl, + Instant.now() + ); + + return photoRepository.save(photo); + } +} diff --git a/src/main/java/com/olhar/olharapi/domain/exception/PhotoNotFoundException.java b/src/main/java/com/olhar/olharapi/domain/exception/PhotoNotFoundException.java new file mode 100644 index 0000000..439ec6d --- /dev/null +++ b/src/main/java/com/olhar/olharapi/domain/exception/PhotoNotFoundException.java @@ -0,0 +1,9 @@ +package com.olhar.olharapi.domain.exception; + +import java.util.UUID; + +public class PhotoNotFoundException extends RuntimeException { + public PhotoNotFoundException(UUID id) { + super("Photo introuvable : " + id); + } +} diff --git a/src/main/java/com/olhar/olharapi/domain/model/Photo.java b/src/main/java/com/olhar/olharapi/domain/model/Photo.java new file mode 100644 index 0000000..079cc99 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/domain/model/Photo.java @@ -0,0 +1,33 @@ +package com.olhar.olharapi.domain.model; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +public record Photo( + UUID id, + String title, + String date, + String place, + List people, + String comment, + Status status, + String by, + Instant valDate, + String flagReason, + String flagBy, + String imageUrl, + Instant createdAt +) { + public enum Status { + TODO, OK, FLAG; + + public String toApiStatus() { + return name().toLowerCase(); + } + + public static Status fromApiStatus(String value) { + return valueOf(value.toUpperCase()); + } + } +} diff --git a/src/main/java/com/olhar/olharapi/infrastructure/persistence/converter/StringListConverter.java b/src/main/java/com/olhar/olharapi/infrastructure/persistence/converter/StringListConverter.java new file mode 100644 index 0000000..9a47148 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/infrastructure/persistence/converter/StringListConverter.java @@ -0,0 +1,34 @@ +package com.olhar.olharapi.infrastructure.persistence.converter; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +import java.util.List; + +@Converter +public class StringListConverter implements AttributeConverter, String> { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(List attribute) { + if (attribute == null || attribute.isEmpty()) return "[]"; + try { + return MAPPER.writeValueAsString(attribute); + } catch (Exception e) { + return "[]"; + } + } + + @Override + public List convertToEntityAttribute(String dbData) { + if (dbData == null || dbData.isBlank()) return List.of(); + try { + return MAPPER.readValue(dbData, new TypeReference<>() {}); + } catch (Exception e) { + return List.of(); + } + } +} diff --git a/src/main/java/com/olhar/olharapi/infrastructure/persistence/entity/PhotoEntity.java b/src/main/java/com/olhar/olharapi/infrastructure/persistence/entity/PhotoEntity.java new file mode 100644 index 0000000..06ba0d0 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/infrastructure/persistence/entity/PhotoEntity.java @@ -0,0 +1,56 @@ +package com.olhar.olharapi.infrastructure.persistence.entity; + +import com.olhar.olharapi.infrastructure.persistence.converter.StringListConverter; +import jakarta.persistence.*; +import lombok.*; + +import java.time.Instant; +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "photos") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PhotoEntity { + + @Id + private UUID id; + + private String title; + + private String date; + + private String place; + + @Convert(converter = StringListConverter.class) + @Column(columnDefinition = "text") + private List people; + + @Column(columnDefinition = "text") + private String comment; + + @Column(nullable = false) + private String status; + + @Column(name = "by_user", nullable = false) + private String by; + + @Column(name = "val_date") + private Instant valDate; + + @Column(name = "flag_reason") + private String flagReason; + + @Column(name = "flag_by") + private String flagBy; + + @Column(name = "image_url", nullable = false) + private String imageUrl; + + @Column(name = "created_at", nullable = false) + private Instant createdAt; +} diff --git a/src/main/java/com/olhar/olharapi/infrastructure/persistence/mapper/PhotoPersistenceMapper.java b/src/main/java/com/olhar/olharapi/infrastructure/persistence/mapper/PhotoPersistenceMapper.java new file mode 100644 index 0000000..e7c7c70 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/infrastructure/persistence/mapper/PhotoPersistenceMapper.java @@ -0,0 +1,45 @@ +package com.olhar.olharapi.infrastructure.persistence.mapper; + +import com.olhar.olharapi.domain.model.Photo; +import com.olhar.olharapi.infrastructure.persistence.entity.PhotoEntity; +import org.springframework.stereotype.Component; + +@Component +public class PhotoPersistenceMapper { + + public Photo toDomain(PhotoEntity entity) { + return new Photo( + entity.getId(), + entity.getTitle(), + entity.getDate(), + entity.getPlace(), + entity.getPeople(), + entity.getComment(), + Photo.Status.valueOf(entity.getStatus()), + entity.getBy(), + entity.getValDate(), + entity.getFlagReason(), + entity.getFlagBy(), + entity.getImageUrl(), + entity.getCreatedAt() + ); + } + + public PhotoEntity toEntity(Photo photo) { + return PhotoEntity.builder() + .id(photo.id()) + .title(photo.title()) + .date(photo.date()) + .place(photo.place()) + .people(photo.people()) + .comment(photo.comment()) + .status(photo.status().name()) + .by(photo.by()) + .valDate(photo.valDate()) + .flagReason(photo.flagReason()) + .flagBy(photo.flagBy()) + .imageUrl(photo.imageUrl()) + .createdAt(photo.createdAt()) + .build(); + } +} diff --git a/src/main/java/com/olhar/olharapi/infrastructure/persistence/repository/JpaPhotoRepository.java b/src/main/java/com/olhar/olharapi/infrastructure/persistence/repository/JpaPhotoRepository.java new file mode 100644 index 0000000..6135eb1 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/infrastructure/persistence/repository/JpaPhotoRepository.java @@ -0,0 +1,9 @@ +package com.olhar.olharapi.infrastructure.persistence.repository; + +import com.olhar.olharapi.infrastructure.persistence.entity.PhotoEntity; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.UUID; + +public interface JpaPhotoRepository extends JpaRepository { +} diff --git a/src/main/java/com/olhar/olharapi/infrastructure/persistence/repository/PhotoRepositoryAdapter.java b/src/main/java/com/olhar/olharapi/infrastructure/persistence/repository/PhotoRepositoryAdapter.java new file mode 100644 index 0000000..176ba0f --- /dev/null +++ b/src/main/java/com/olhar/olharapi/infrastructure/persistence/repository/PhotoRepositoryAdapter.java @@ -0,0 +1,34 @@ +package com.olhar.olharapi.infrastructure.persistence.repository; + +import com.olhar.olharapi.application.port.out.PhotoRepository; +import com.olhar.olharapi.domain.model.Photo; +import com.olhar.olharapi.infrastructure.persistence.mapper.PhotoPersistenceMapper; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Component +@RequiredArgsConstructor +public class PhotoRepositoryAdapter implements PhotoRepository { + + private final JpaPhotoRepository jpa; + private final PhotoPersistenceMapper mapper; + + @Override + public List findAll() { + return jpa.findAll().stream().map(mapper::toDomain).toList(); + } + + @Override + public Optional findById(UUID id) { + return jpa.findById(id).map(mapper::toDomain); + } + + @Override + public Photo save(Photo photo) { + return mapper.toDomain(jpa.save(mapper.toEntity(photo))); + } +} diff --git a/src/main/java/com/olhar/olharapi/infrastructure/storage/LocalPhotoStorage.java b/src/main/java/com/olhar/olharapi/infrastructure/storage/LocalPhotoStorage.java new file mode 100644 index 0000000..38dbc34 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/infrastructure/storage/LocalPhotoStorage.java @@ -0,0 +1,62 @@ +package com.olhar.olharapi.infrastructure.storage; + +import com.olhar.olharapi.application.port.out.PhotoStoragePort; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.UUID; + +@Component +public class LocalPhotoStorage implements PhotoStoragePort { + + private final Path storageRoot; + + public LocalPhotoStorage(@Value("${photos.storage-path:/app/uploads}") String storagePath) { + this.storageRoot = Path.of(storagePath); + try { + Files.createDirectories(storageRoot); + } catch (IOException e) { + throw new UncheckedIOException("Impossible de créer le répertoire de stockage", e); + } + } + + @Override + public String store(UUID photoId, String filename, InputStream data) { + String ext = extractExtension(filename); + Path target = storageRoot.resolve(photoId + ext); + try { + Files.copy(data, target, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + throw new UncheckedIOException("Échec de l'écriture du fichier", e); + } + return "/api/v1/photos/" + photoId + "/image"; + } + + @Override + public InputStream load(UUID photoId) { + try { + return Files.list(storageRoot) + .filter(p -> p.getFileName().toString().startsWith(photoId.toString())) + .findFirst() + .map(p -> { + try { return Files.newInputStream(p); } + catch (IOException e) { throw new UncheckedIOException(e); } + }) + .orElseThrow(() -> new IllegalArgumentException("Fichier introuvable : " + photoId)); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + private String extractExtension(String filename) { + if (filename == null) return ".jpg"; + int dot = filename.lastIndexOf('.'); + return dot >= 0 ? filename.substring(dot) : ".jpg"; + } +} diff --git a/src/main/java/com/olhar/olharapi/interfaces/exception/GlobalExceptionHandler.java b/src/main/java/com/olhar/olharapi/interfaces/exception/GlobalExceptionHandler.java index ce50b7e..624d353 100644 --- a/src/main/java/com/olhar/olharapi/interfaces/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/olhar/olharapi/interfaces/exception/GlobalExceptionHandler.java @@ -1,6 +1,7 @@ package com.olhar.olharapi.interfaces.exception; import com.olhar.olharapi.domain.exception.EmailAlreadyUsedException; +import com.olhar.olharapi.domain.exception.PhotoNotFoundException; import com.olhar.olharapi.domain.exception.UserNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ProblemDetail; @@ -26,6 +27,11 @@ public class GlobalExceptionHandler { return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); } + @ExceptionHandler(PhotoNotFoundException.class) + public ProblemDetail handlePhotoNotFound(PhotoNotFoundException ex) { + return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage()); + } + @ExceptionHandler(BadCredentialsException.class) public ProblemDetail handleBadCredentials(BadCredentialsException ex) { return ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, ex.getMessage()); diff --git a/src/main/java/com/olhar/olharapi/interfaces/rest/controller/PhotoController.java b/src/main/java/com/olhar/olharapi/interfaces/rest/controller/PhotoController.java new file mode 100644 index 0000000..9ab1eea --- /dev/null +++ b/src/main/java/com/olhar/olharapi/interfaces/rest/controller/PhotoController.java @@ -0,0 +1,82 @@ +package com.olhar.olharapi.interfaces.rest.controller; + +import com.olhar.olharapi.application.port.in.GetPhotosUseCase; +import com.olhar.olharapi.application.port.in.UpdatePhotoUseCase; +import com.olhar.olharapi.application.port.in.UploadPhotosUseCase; +import com.olhar.olharapi.application.port.out.PhotoStoragePort; +import com.olhar.olharapi.interfaces.rest.dto.request.UpdatePhotoRequest; +import com.olhar.olharapi.interfaces.rest.dto.response.PhotoResponse; +import com.olhar.olharapi.interfaces.rest.mapper.PhotoRestMapper; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.UUID; + +@RestController +@RequestMapping("/api/v1/photos") +@RequiredArgsConstructor +@Tag(name = "Photos", description = "Gestion des photos") +public class PhotoController { + + private final GetPhotosUseCase getPhotosUseCase; + private final UpdatePhotoUseCase updatePhotoUseCase; + private final UploadPhotosUseCase uploadPhotosUseCase; + private final PhotoStoragePort photoStorage; + private final PhotoRestMapper mapper; + + @GetMapping + @Operation(summary = "Lister toutes les photos") + public List getAll() { + return getPhotosUseCase.getAll().stream().map(mapper::toResponse).toList(); + } + + @PatchMapping("/{id}") + @Operation(summary = "Mettre à jour les métadonnées ou le statut d'une photo") + public PhotoResponse update(@PathVariable UUID id, @RequestBody UpdatePhotoRequest request) { + return mapper.toResponse(updatePhotoUseCase.update(id, + new UpdatePhotoUseCase.Command( + request.title(), request.date(), request.place(), request.people(), + request.comment(), request.status(), request.flagReason(), request.flagBy() + ))); + } + + @PostMapping(consumes = MediaType.MULTIPART_FORM_DATA_VALUE) + @ResponseStatus(HttpStatus.CREATED) + @Operation(summary = "Importer des photos depuis la galerie") + public List upload( + @RequestParam("files[]") List files, + Authentication authentication) throws IOException { + + List entries = files.stream() + .map(f -> { + try { + return new UploadPhotosUseCase.FileEntry( + f.getOriginalFilename(), f.getContentType(), f.getInputStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .toList(); + + return uploadPhotosUseCase.upload(entries, authentication.getName()) + .stream().map(mapper::toResponse).toList(); + } + + @GetMapping("/{id}/image") + @Operation(summary = "Télécharger l'image d'une photo") + public ResponseEntity getImage(@PathVariable UUID id) throws IOException { + byte[] data = photoStorage.load(id).readAllBytes(); + return ResponseEntity.ok() + .contentType(MediaType.IMAGE_JPEG) + .body(data); + } +} diff --git a/src/main/java/com/olhar/olharapi/interfaces/rest/dto/request/UpdatePhotoRequest.java b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/request/UpdatePhotoRequest.java new file mode 100644 index 0000000..d9fa70a --- /dev/null +++ b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/request/UpdatePhotoRequest.java @@ -0,0 +1,14 @@ +package com.olhar.olharapi.interfaces.rest.dto.request; + +import java.util.List; + +public record UpdatePhotoRequest( + String title, + String date, + String place, + List people, + String comment, + String status, + String flagReason, + String flagBy +) {} diff --git a/src/main/java/com/olhar/olharapi/interfaces/rest/dto/response/PhotoResponse.java b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/response/PhotoResponse.java new file mode 100644 index 0000000..396c8c1 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/interfaces/rest/dto/response/PhotoResponse.java @@ -0,0 +1,18 @@ +package com.olhar.olharapi.interfaces.rest.dto.response; + +import java.util.List; + +public record PhotoResponse( + String id, + String title, + String date, + String place, + List people, + String comment, + String status, + String by, + String valDate, + String flagReason, + String flagBy, + String imageUrl +) {} diff --git a/src/main/java/com/olhar/olharapi/interfaces/rest/mapper/PhotoRestMapper.java b/src/main/java/com/olhar/olharapi/interfaces/rest/mapper/PhotoRestMapper.java new file mode 100644 index 0000000..b405130 --- /dev/null +++ b/src/main/java/com/olhar/olharapi/interfaces/rest/mapper/PhotoRestMapper.java @@ -0,0 +1,36 @@ +package com.olhar.olharapi.interfaces.rest.mapper; + +import com.olhar.olharapi.domain.model.Photo; +import com.olhar.olharapi.interfaces.rest.dto.response.PhotoResponse; +import org.springframework.stereotype.Component; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; + +@Component +public class PhotoRestMapper { + + private static final DateTimeFormatter ISO = DateTimeFormatter.ISO_INSTANT; + + public PhotoResponse toResponse(Photo photo) { + return new PhotoResponse( + photo.id().toString(), + photo.title(), + photo.date(), + photo.place(), + photo.people(), + photo.comment(), + photo.status().toApiStatus(), + photo.by(), + formatInstant(photo.valDate()), + photo.flagReason(), + photo.flagBy(), + photo.imageUrl() + ); + } + + private String formatInstant(Instant instant) { + return instant != null ? ISO.format(instant.atOffset(ZoneOffset.UTC)) : null; + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 07b5b80..8b4e85e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,6 +19,10 @@ spring: server: port: 8080 +spring.servlet.multipart: + max-file-size: 50MB + max-request-size: 200MB + springdoc: api-docs: path: /api-docs @@ -33,3 +37,6 @@ security: cors: allowed-origin: ${CORS_ALLOWED_ORIGIN_PROD:http://localhost:4200} + +photos: + storage-path: ${PHOTOS_STORAGE_PATH:/app/uploads} diff --git a/src/main/resources/db/migration/V3__create_photos_table.sql b/src/main/resources/db/migration/V3__create_photos_table.sql new file mode 100644 index 0000000..4a0b3f3 --- /dev/null +++ b/src/main/resources/db/migration/V3__create_photos_table.sql @@ -0,0 +1,15 @@ +CREATE TABLE photos ( + id UUID PRIMARY KEY, + title VARCHAR(255), + date VARCHAR(100), + place VARCHAR(255), + people TEXT, + comment TEXT, + status VARCHAR(20) NOT NULL DEFAULT 'TODO', + by_user VARCHAR(255) NOT NULL, + val_date TIMESTAMPTZ, + flag_reason TEXT, + flag_by VARCHAR(255), + image_url VARCHAR(500) NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); diff --git a/src/test/java/com/olhar/olharapi/application/usecase/UpdatePhotoServiceTest.java b/src/test/java/com/olhar/olharapi/application/usecase/UpdatePhotoServiceTest.java new file mode 100644 index 0000000..99f1057 --- /dev/null +++ b/src/test/java/com/olhar/olharapi/application/usecase/UpdatePhotoServiceTest.java @@ -0,0 +1,74 @@ +package com.olhar.olharapi.application.usecase; + +import com.olhar.olharapi.application.port.out.PhotoRepository; +import com.olhar.olharapi.application.port.in.UpdatePhotoUseCase; +import com.olhar.olharapi.domain.exception.PhotoNotFoundException; +import com.olhar.olharapi.domain.model.Photo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +class UpdatePhotoServiceTest { + + private PhotoRepository photoRepository; + private UpdatePhotoService service; + + @BeforeEach + void setUp() { + photoRepository = mock(PhotoRepository.class); + service = new UpdatePhotoService(photoRepository); + } + + @Test + void update_shouldSetValDate_whenStatusBecomesOk() { + UUID id = UUID.randomUUID(); + Photo existing = photoWithStatus(id, Photo.Status.TODO); + when(photoRepository.findById(id)).thenReturn(Optional.of(existing)); + when(photoRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + Photo result = service.update(id, new UpdatePhotoUseCase.Command( + null, null, null, null, null, "ok", null, null)); + + assertThat(result.status()).isEqualTo(Photo.Status.OK); + assertThat(result.valDate()).isNotNull(); + } + + @Test + void update_shouldClearValDate_whenStatusBecomesFlag() { + UUID id = UUID.randomUUID(); + Photo existing = photoWithStatus(id, Photo.Status.OK); + when(photoRepository.findById(id)).thenReturn(Optional.of(existing)); + when(photoRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + + Photo result = service.update(id, new UpdatePhotoUseCase.Command( + null, null, null, null, null, "flag", "Bad photo", "gato")); + + assertThat(result.status()).isEqualTo(Photo.Status.FLAG); + assertThat(result.valDate()).isNull(); + } + + @Test + void update_shouldThrow_whenPhotoNotFound() { + UUID id = UUID.randomUUID(); + when(photoRepository.findById(id)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> service.update(id, new UpdatePhotoUseCase.Command( + null, null, null, null, null, null, null, null))) + .isInstanceOf(PhotoNotFoundException.class); + } + + private Photo photoWithStatus(UUID id, Photo.Status status) { + return new Photo(id, "title", "2026-06-07", "Paris", List.of(), + null, status, "gato", null, null, null, + "/api/v1/photos/" + id + "/image", Instant.now()); + } +}