From 4d7f452a9794de949f9a56ecb7658f539f90e71b Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Tue, 7 Apr 2026 09:15:01 -0600 Subject: [PATCH] add my pets api --- .../backend/controller/MyPetController.java | 76 ++++++++++++++++++ .../petshop/backend/dto/pet/MyPetRequest.java | 42 ++++++++++ .../backend/dto/pet/MyPetResponse.java | 61 +++++++++++++++ .../backend/repository/PetRepository.java | 3 + .../petshop/backend/service/PetService.java | 78 +++++++++++++++++++ 5 files changed, 260 insertions(+) create mode 100644 backend/src/main/java/com/petshop/backend/controller/MyPetController.java create mode 100644 backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java create mode 100644 backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java diff --git a/backend/src/main/java/com/petshop/backend/controller/MyPetController.java b/backend/src/main/java/com/petshop/backend/controller/MyPetController.java new file mode 100644 index 00000000..e43dbc42 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/MyPetController.java @@ -0,0 +1,76 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.dto.pet.MyPetRequest; +import com.petshop.backend.dto.pet.MyPetResponse; +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.service.PetService; +import com.petshop.backend.util.AuthenticationHelper; +import jakarta.validation.Valid; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/v1/my-pets") +@PreAuthorize("isAuthenticated()") +public class MyPetController { + + private final PetService petService; + private final UserRepository userRepository; + + public MyPetController(PetService petService, UserRepository userRepository) { + this.petService = petService; + this.userRepository = userRepository; + } + + @GetMapping + public ResponseEntity> getMyPets() { + return ResponseEntity.ok(petService.getMyPets(currentUserId())); + } + + @PostMapping + public ResponseEntity createMyPet(@Valid @RequestBody MyPetRequest request) { + return ResponseEntity.ok(petService.createMyPet(currentUserId(), request)); + } + + @PutMapping("/{id}") + public ResponseEntity updateMyPet(@PathVariable Long id, @Valid @RequestBody MyPetRequest request) { + return ResponseEntity.ok(petService.updateMyPet(currentUserId(), id, request)); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteMyPet(@PathVariable Long id) { + petService.deleteMyPet(currentUserId(), id); + return ResponseEntity.noContent().build(); + } + + @PostMapping("/{id}/image") + public ResponseEntity uploadMyPetImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) { + try { + return ResponseEntity.ok(petService.uploadMyPetImage(currentUserId(), id, image)); + } catch (IllegalArgumentException ex) { + return ResponseEntity.badRequest().body(Map.of("message", ex.getMessage())); + } catch (IOException ex) { + return ResponseEntity.badRequest().body(Map.of("message", "Failed to upload pet image: " + ex.getMessage())); + } + } + + private Long currentUserId() { + User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + return user.getId(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java new file mode 100644 index 00000000..17d08e2f --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetRequest.java @@ -0,0 +1,42 @@ +package com.petshop.backend.dto.pet; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class MyPetRequest { + + @NotBlank(message = "Pet name is required") + @Size(max = 50, message = "Pet name must not exceed 50 characters") + private String petName; + + @NotBlank(message = "Species is required") + @Size(max = 50, message = "Species must not exceed 50 characters") + private String species; + + @Size(max = 50, message = "Breed must not exceed 50 characters") + private String breed; + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getSpecies() { + return species; + } + + public void setSpecies(String species) { + this.species = species; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } +} diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java new file mode 100644 index 00000000..7063b2f8 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/dto/pet/MyPetResponse.java @@ -0,0 +1,61 @@ +package com.petshop.backend.dto.pet; + +public class MyPetResponse { + + private Long customerPetId; + private String petName; + private String species; + private String breed; + private String imageUrl; + + public MyPetResponse() { + } + + public MyPetResponse(Long customerPetId, String petName, String species, String breed, String imageUrl) { + this.customerPetId = customerPetId; + this.petName = petName; + this.species = species; + this.breed = breed; + this.imageUrl = imageUrl; + } + + public Long getCustomerPetId() { + return customerPetId; + } + + public void setCustomerPetId(Long customerPetId) { + this.customerPetId = customerPetId; + } + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getSpecies() { + return species; + } + + public void setSpecies(String species) { + this.species = species; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getImageUrl() { + return imageUrl; + } + + public void setImageUrl(String imageUrl) { + this.imageUrl = imageUrl; + } +} diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index fd55fabe..73f05460 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -9,11 +9,14 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Optional; @Repository public interface PetRepository extends JpaRepository { List findAllByPetStatusIgnoreCaseOrderByPetNameAsc(String petStatus); + List findAllByOwner_IdOrderByPetNameAsc(Long ownerId); + Optional findByIdAndOwner_Id(Long id, Long ownerId); @Query("SELECT p FROM Pet p WHERE " + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index 09dd402b..bbd39085 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -1,6 +1,8 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.dto.pet.MyPetRequest; +import com.petshop.backend.dto.pet.MyPetResponse; import com.petshop.backend.dto.pet.PetRequest; import com.petshop.backend.dto.pet.PetResponse; import com.petshop.backend.entity.Adoption; @@ -25,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; +import java.util.List; import java.util.Locale; @Service @@ -82,6 +85,50 @@ public class PetService { return mapToResponse(pet); } + @Transactional(readOnly = true) + public List getMyPets(Long ownerUserId) { + return petRepository.findAllByOwner_IdOrderByPetNameAsc(ownerUserId).stream() + .map(this::mapToMyPetResponse) + .toList(); + } + + @Transactional + public MyPetResponse createMyPet(Long ownerUserId, MyPetRequest request) { + User owner = userRepository.findById(ownerUserId) + .orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + ownerUserId)); + Pet pet = new Pet(); + pet.setOwner(owner); + pet.setStore(null); + pet.setPetStatus("Owned"); + applyMyPetRequest(pet, request); + return mapToMyPetResponse(petRepository.save(pet)); + } + + @Transactional + public MyPetResponse updateMyPet(Long ownerUserId, Long petId, MyPetRequest request) { + Pet pet = findOwnedPet(ownerUserId, petId); + pet.setPetStatus("Owned"); + pet.setStore(null); + applyMyPetRequest(pet, request); + return mapToMyPetResponse(petRepository.save(pet)); + } + + @Transactional + public void deleteMyPet(Long ownerUserId, Long petId) { + Pet pet = findOwnedPet(ownerUserId, petId); + deleteStoredImageIfPresent(pet.getImageUrl()); + petRepository.delete(pet); + } + + @Transactional + public MyPetResponse uploadMyPetImage(Long ownerUserId, Long petId, MultipartFile file) throws IOException { + validateImageFile(file); + Pet pet = findOwnedPet(ownerUserId, petId); + deleteStoredImageIfPresent(pet.getImageUrl()); + pet.setImageUrl(catalogImageStorageService.storePetImage(file)); + return mapToMyPetResponse(petRepository.save(pet)); + } + @Transactional public PetResponse createPet(PetRequest request) { Pet pet = new Pet(); @@ -225,6 +272,11 @@ public class PetService { } } + private Pet findOwnedPet(Long ownerUserId, Long petId) { + return petRepository.findByIdAndOwner_Id(petId, ownerUserId) + .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + petId)); + } + private void deleteStoredImageIfPresent(String storedImagePath) { if (storedImagePath == null || storedImagePath.isBlank()) { return; @@ -276,6 +328,32 @@ public class PetService { ); } + private MyPetResponse mapToMyPetResponse(Pet pet) { + return new MyPetResponse( + pet.getPetId(), + pet.getPetName(), + pet.getPetSpecies(), + pet.getPetBreed(), + pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null + ); + } + + private void applyMyPetRequest(Pet pet, MyPetRequest request) { + pet.setPetName(request.getPetName().trim()); + pet.setPetSpecies(request.getSpecies().trim()); + pet.setPetBreed(normalizeOptional(request.getBreed())); + pet.setPetAge(null); + pet.setPetPrice(null); + } + + private String normalizeOptional(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } + private void applyOwnerAndStore(Pet pet, PetRequest request) { if ("owned".equalsIgnoreCase(request.getPetStatus())) { if (request.getCustomerId() != null) {