Pet owner and store

This commit is contained in:
2026-04-06 15:50:45 -06:00
parent a66a779bcb
commit 0cc4a2bedd
9 changed files with 236 additions and 33 deletions

View File

@@ -27,8 +27,9 @@ public class PetController {
@RequestParam(required = false) String q,
@RequestParam(required = false) String species,
@RequestParam(required = false) String status,
@RequestParam(required = false) Long storeId,
Pageable pageable) {
return ResponseEntity.ok(petService.getAllPets(q, species, status, pageable));
return ResponseEntity.ok(petService.getAllPets(q, species, status, storeId, pageable));
}
@GetMapping("/{id}")

View File

@@ -23,6 +23,10 @@ public class PetRequest {
private BigDecimal petPrice;
private Long customerId;
private Long storeId;
public String getPetName() {
return petName;
}
@@ -71,6 +75,22 @@ public class PetRequest {
this.petPrice = petPrice;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@@ -15,11 +15,15 @@ public class PetResponse {
private String imageUrl;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private Long customerId;
private String customerName;
private Long storeId;
private String storeName;
public PetResponse() {
}
public PetResponse(Long petId, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt) {
public PetResponse(Long petId, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt, Long customerId, String customerName, Long storeId, String storeName) {
this.petId = petId;
this.petName = petName;
this.petSpecies = petSpecies;
@@ -30,6 +34,10 @@ public class PetResponse {
this.imageUrl = imageUrl;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
this.customerId = customerId;
this.customerName = customerName;
this.storeId = storeId;
this.storeName = storeName;
}
public Long getPetId() {
@@ -112,17 +120,49 @@ public class PetResponse {
this.updatedAt = updatedAt;
}
public Long getCustomerId() {
return customerId;
}
public void setCustomerId(Long customerId) {
this.customerId = customerId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public Long getStoreId() {
return storeId;
}
public void setStoreId(Long storeId) {
this.storeId = storeId;
}
public String getStoreName() {
return storeName;
}
public void setStoreName(String storeName) {
this.storeName = storeName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PetResponse that = (PetResponse) o;
return Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(petSpecies, that.petSpecies) && Objects.equals(petBreed, that.petBreed) && Objects.equals(petAge, that.petAge) && Objects.equals(petStatus, that.petStatus) && Objects.equals(petPrice, that.petPrice) && Objects.equals(imageUrl, that.imageUrl) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
return Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(petSpecies, that.petSpecies) && Objects.equals(petBreed, that.petBreed) && Objects.equals(petAge, that.petAge) && Objects.equals(petStatus, that.petStatus) && Objects.equals(petPrice, that.petPrice) && Objects.equals(imageUrl, that.imageUrl) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt) && Objects.equals(customerId, that.customerId) && Objects.equals(customerName, that.customerName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName);
}
@Override
public int hashCode() {
return Objects.hash(petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, createdAt, updatedAt);
return Objects.hash(petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, createdAt, updatedAt, customerId, customerName, storeId, storeName);
}
@Override
@@ -138,6 +178,10 @@ public class PetResponse {
", imageUrl='" + imageUrl + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
", customerId=" + customerId +
", customerName='" + customerName + '\'' +
", storeId=" + storeId +
", storeName='" + storeName + '\'' +
'}';
}
}

View File

@@ -38,6 +38,14 @@ public class Pet {
@Column(length = 255)
private String imageUrl;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "customerId")
private Customer customer;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "storeId")
private StoreLocation store;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@@ -142,6 +150,22 @@ public class Pet {
this.updatedAt = updatedAt;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
public StoreLocation getStore() {
return store;
}
public void setStore(StoreLocation store) {
this.store = store;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@@ -18,16 +18,18 @@ public interface PetRepository extends JpaRepository<Pet, Long> {
@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(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable);
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status)) AND " +
"(:storeId IS NULL OR p.store.storeId = :storeId)")
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, @Param("storeId") Long storeId, Pageable pageable);
@Query("SELECT p FROM Pet p WHERE LOWER(p.petStatus) = 'available' AND " +
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species))")
Page<Pet> searchPublicPets(@Param("q") String query, @Param("species") String species, Pageable pageable);
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:storeId IS NULL OR p.store.storeId = :storeId)")
Page<Pet> searchPublicPets(@Param("q") String query, @Param("species") String species, @Param("storeId") Long storeId, Pageable pageable);
@Query("SELECT DISTINCT p FROM Pet p LEFT JOIN Adoption a ON a.pet = p AND LOWER(a.adoptionStatus) = 'completed' WHERE " +
"(LOWER(p.petStatus) = 'available' OR a.customer.userId = :userId) AND " +
"(LOWER(p.petStatus) = 'available' OR a.customer.userId = :userId OR (LOWER(p.petStatus) = 'owned' AND p.customer.userId = :userId)) AND " +
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petBreed) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")

View File

@@ -4,12 +4,16 @@ import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.dto.pet.PetRequest;
import com.petshop.backend.dto.pet.PetResponse;
import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.security.AppPrincipal;
import com.petshop.backend.repository.AdoptionRepository;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.StoreRepository;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.data.domain.Page;
@@ -29,15 +33,20 @@ public class PetService {
private final PetRepository petRepository;
private final AdoptionRepository adoptionRepository;
private final CustomerRepository customerRepository;
private final StoreRepository storeRepository;
private final CatalogImageStorageService catalogImageStorageService;
public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, CatalogImageStorageService catalogImageStorageService) {
public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, CustomerRepository customerRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) {
this.petRepository = petRepository;
this.adoptionRepository = adoptionRepository;
this.customerRepository = customerRepository;
this.storeRepository = storeRepository;
this.catalogImageStorageService = catalogImageStorageService;
}
public Page<PetResponse> getAllPets(String query, String species, String status, Pageable pageable) {
@Transactional(readOnly = true)
public Page<PetResponse> getAllPets(String query, String species, String status, Long storeId, Pageable pageable) {
String normalizedQuery = normalizeFilter(query);
String normalizedSpecies = normalizeFilter(species);
String normalizedStatus = normalizeFilter(status);
@@ -48,22 +57,23 @@ public class PetService {
if (!isAllowedPublicStatus(normalizedStatus)) {
return new PageImpl<>(java.util.List.of(), pageable, 0);
}
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable);
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, storeId, pageable);
} else if (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN) {
pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, pageable);
pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, storeId, pageable);
} else if (viewer.role() == User.Role.CUSTOMER) {
if (!isAllowedCustomerStatus(normalizedStatus)) {
return new PageImpl<>(java.util.List.of(), pageable, 0);
}
pets = petRepository.searchCustomerVisiblePets(viewer.userId(), normalizedQuery, normalizedSpecies, normalizedStatus, pageable);
} else {
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, pageable);
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, storeId, pageable);
}
return pets
.map(this::mapToResponse);
}
@Transactional(readOnly = true)
public PetResponse getPetById(Long id) {
Pet pet = petRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
@@ -82,6 +92,7 @@ public class PetService {
pet.setPetAge(request.getPetAge());
pet.setPetStatus(request.getPetStatus());
pet.setPetPrice(request.getPetPrice());
applyOwnerAndStore(pet, request);
pet = petRepository.save(pet);
return mapToResponse(pet);
@@ -98,6 +109,7 @@ public class PetService {
pet.setPetAge(request.getPetAge());
pet.setPetStatus(request.getPetStatus());
pet.setPetPrice(request.getPetPrice());
applyOwnerAndStore(pet, request);
pet = petRepository.save(pet);
return mapToResponse(pet);
@@ -161,6 +173,9 @@ public class PetService {
if (viewer == null || viewer.userId() == null) {
return false;
}
if (isOwnedByUser(pet, viewer.userId())) {
return true;
}
return isAdoptedByUser(pet, viewer.userId());
}
@@ -230,7 +245,7 @@ public class PetService {
}
private boolean isAllowedCustomerStatus(String status) {
return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status);
return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status) || "owned".equalsIgnoreCase(status);
}
private String normalizeFilter(String value) {
@@ -242,6 +257,8 @@ public class PetService {
}
private PetResponse mapToResponse(Pet pet) {
Customer customer = pet.getCustomer();
StoreLocation store = pet.getStore();
return new PetResponse(
pet.getPetId(),
pet.getPetName(),
@@ -252,10 +269,59 @@ public class PetService {
pet.getPetPrice(),
pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null,
pet.getCreatedAt(),
pet.getUpdatedAt()
pet.getUpdatedAt(),
customer != null ? customer.getCustomerId() : null,
customer != null ? customer.getFirstName() + " " + customer.getLastName() : null,
store != null ? store.getStoreId() : null,
store != null ? store.getStoreName() : null
);
}
private void applyOwnerAndStore(Pet pet, PetRequest request) {
if ("owned".equalsIgnoreCase(request.getPetStatus())) {
if (request.getCustomerId() != null) {
Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
pet.setCustomer(customer);
} else {
pet.setCustomer(null);
}
pet.setStore(null);
} else if ("available".equalsIgnoreCase(request.getPetStatus()) || "unadopted".equalsIgnoreCase(request.getPetStatus())) {
if (request.getStoreId() != null) {
StoreLocation store = storeRepository.findById(request.getStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
pet.setStore(store);
} else {
pet.setStore(null);
}
pet.setCustomer(null);
} else {
if (request.getCustomerId() != null) {
Customer customer = customerRepository.findById(request.getCustomerId())
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + request.getCustomerId()));
pet.setCustomer(customer);
} else {
pet.setCustomer(null);
}
if (request.getStoreId() != null) {
StoreLocation store = storeRepository.findById(request.getStoreId())
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getStoreId()));
pet.setStore(store);
} else {
pet.setStore(null);
}
}
}
private boolean isOwnedByUser(Pet pet, Long userId) {
if (!"owned".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) {
return false;
}
Customer customer = pet.getCustomer();
return customer != null && userId.equals(customer.getUserId());
}
public record ImagePayload(Resource resource, MediaType mediaType) {
}

View File

@@ -0,0 +1,23 @@
ALTER TABLE pet ADD COLUMN customerId BIGINT NULL;
ALTER TABLE pet ADD COLUMN storeId BIGINT NULL;
ALTER TABLE pet ADD CONSTRAINT fk_pet_customer
FOREIGN KEY (customerId) REFERENCES customer(customerId);
ALTER TABLE pet ADD CONSTRAINT fk_pet_store
FOREIGN KEY (storeId) REFERENCES storeLocation(storeId);
CREATE INDEX idx_pet_customerId ON pet(customerId);
CREATE INDEX idx_pet_storeId ON pet(storeId);
UPDATE pet
SET storeId = (SELECT storeId FROM storeLocation ORDER BY storeId ASC LIMIT 1)
WHERE LOWER(petStatus) IN ('available', 'unadopted');
UPDATE pet p
JOIN (
SELECT a.petId, a.customerId
FROM adoption a
WHERE LOWER(a.adoptionStatus) = 'completed'
) latest ON latest.petId = p.petId
SET p.customerId = latest.customerId
WHERE LOWER(p.petStatus) = 'adopted';