From 0cc4a2bedd5f843d2580f531932e172baa821483 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 15:50:45 -0600 Subject: [PATCH 1/3] Pet owner and store --- backend/petshop-api.postman_collection.json | 29 +++++-- .../backend/controller/PetController.java | 3 +- .../petshop/backend/dto/pet/PetRequest.java | 20 +++++ .../petshop/backend/dto/pet/PetResponse.java | 50 +++++++++++- .../java/com/petshop/backend/entity/Pet.java | 24 ++++++ .../backend/repository/PetRepository.java | 12 +-- .../petshop/backend/service/PetService.java | 80 +++++++++++++++++-- .../db/migration/V19__pet_owner_and_store.sql | 23 ++++++ .../backend/service/PetServiceTest.java | 28 ++++--- 9 files changed, 236 insertions(+), 33 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql diff --git a/backend/petshop-api.postman_collection.json b/backend/petshop-api.postman_collection.json index 73fed551..1068f4e0 100644 --- a/backend/petshop-api.postman_collection.json +++ b/backend/petshop-api.postman_collection.json @@ -564,7 +564,7 @@ "name": "Get All Pets", "request": { "method": "GET", - "url": "{{baseUrl}}/api/v1/pets", + "url": "{{baseUrl}}/api/v1/pets?status=available&storeId=1", "header": [ { "key": "Content-Type", @@ -585,7 +585,10 @@ "exec": [ "pm.test('Status code is 200', function () {", " pm.response.to.have.status(200);", - "});" + "});", + "var json = pm.response.json();", + "pm.test('is page response', function () { pm.expect(json.content).to.be.an('array'); });", + "pm.test('all pets have storeName', function () { json.content.forEach(function(p) { pm.expect(p).to.have.property('storeName'); }); });" ] } } @@ -616,7 +619,10 @@ "exec": [ "pm.test('Status code is 200', function () {", " pm.response.to.have.status(200);", - "});" + "});", + "var json = pm.response.json();", + "pm.test('has petId', function () { pm.expect(json.petId).to.be.a('number'); });", + "pm.test('has owner fields', function () { pm.expect(json).to.have.all.keys('petId','petName','petSpecies','petBreed','petAge','petStatus','petPrice','imageUrl','createdAt','updatedAt','customerId','customerName','storeId','storeName'); });" ] } } @@ -671,7 +677,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petName\": \"Postman Pet\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 2,\n \"petStatus\": \"Available\",\n \"petPrice\": 350.00\n}", + "raw": "{\n \"petName\": \"Postman Pet\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 2,\n \"petStatus\": \"Available\",\n \"petPrice\": 350.00,\n \"storeId\": 1\n}", "options": { "raw": { "language": "json" @@ -689,7 +695,11 @@ " pm.response.to.have.status(201);", "});", "var jsonData = pm.response.json();", - "if (jsonData.petId !== undefined) pm.collectionVariables.set('petId', jsonData.petId);" + "if (jsonData.petId !== undefined) pm.collectionVariables.set('petId', jsonData.petId);", + "pm.test('has petId', function () { pm.expect(jsonData.petId).to.be.a('number'); });", + "pm.test('has storeId', function () { pm.expect(jsonData.storeId).to.equal(1); });", + "pm.test('has storeName', function () { pm.expect(jsonData.storeName).to.be.a('string'); });", + "pm.test('customerId is null', function () { pm.expect(jsonData.customerId).to.be.null; });" ] } } @@ -713,7 +723,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"petName\": \"Postman Pet Updated\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 3,\n \"petStatus\": \"Available\",\n \"petPrice\": 375.00\n}", + "raw": "{\n \"petName\": \"Postman Pet Updated\",\n \"petSpecies\": \"Dog\",\n \"petBreed\": \"Mixed\",\n \"petAge\": 3,\n \"petStatus\": \"Owned\",\n \"petPrice\": 375.00,\n \"customerId\": 1\n}", "options": { "raw": { "language": "json" @@ -729,7 +739,12 @@ "exec": [ "pm.test('Status code is 200', function () {", " pm.response.to.have.status(200);", - "});" + "});", + "var json = pm.response.json();", + "pm.test('status is Owned', function () { pm.expect(json.petStatus).to.equal('Owned'); });", + "pm.test('has customerId', function () { pm.expect(json.customerId).to.be.a('number'); });", + "pm.test('has customerName', function () { pm.expect(json.customerName).to.be.a('string'); });", + "pm.test('storeId is null', function () { pm.expect(json.storeId).to.be.null; });" ] } } diff --git a/backend/src/main/java/com/petshop/backend/controller/PetController.java b/backend/src/main/java/com/petshop/backend/controller/PetController.java index 9fb93f0b..60f9a5e4 100644 --- a/backend/src/main/java/com/petshop/backend/controller/PetController.java +++ b/backend/src/main/java/com/petshop/backend/controller/PetController.java @@ -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}") diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java index db3f71c9..9a92581a 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java @@ -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; diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/PetResponse.java b/backend/src/main/java/com/petshop/backend/dto/pet/PetResponse.java index e3213653..b7113bb1 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/PetResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/PetResponse.java @@ -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 + '\'' + '}'; } } diff --git a/backend/src/main/java/com/petshop/backend/entity/Pet.java b/backend/src/main/java/com/petshop/backend/entity/Pet.java index 8f6a6020..d0b3b3fc 100644 --- a/backend/src/main/java/com/petshop/backend/entity/Pet.java +++ b/backend/src/main/java/com/petshop/backend/entity/Pet.java @@ -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; 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 468c0c9d..d01c2d85 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -18,16 +18,18 @@ public interface PetRepository extends JpaRepository { @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 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 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 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 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))") 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 dc5fa61e..7d038ed4 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -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 getAllPets(String query, String species, String status, Pageable pageable) { + @Transactional(readOnly = true) + public Page 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) { } diff --git a/backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql b/backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql new file mode 100644 index 00000000..918ed375 --- /dev/null +++ b/backend/src/main/resources/db/migration/V19__pet_owner_and_store.sql @@ -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'; diff --git a/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java b/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java index 9107ebd9..6a80e9ca 100644 --- a/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/PetServiceTest.java @@ -6,7 +6,9 @@ import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; 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 com.petshop.backend.security.AppPrincipal; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -39,6 +41,12 @@ class PetServiceTest { @Mock private AdoptionRepository adoptionRepository; + @Mock + private CustomerRepository customerRepository; + + @Mock + private StoreRepository storeRepository; + @Mock private CatalogImageStorageService catalogImageStorageService; @@ -54,24 +62,24 @@ class PetServiceTest { void getAllPetsAnonymousReturnsOnlyPublicPets() { Pageable pageable = PageRequest.of(0, 10); Pet availablePet = pet(1L, "Buddy", "Available"); - when(petRepository.searchPublicPets(null, null, pageable)).thenReturn(new PageImpl<>(List.of(availablePet), pageable, 1)); + when(petRepository.searchPublicPets(null, null, null, pageable)).thenReturn(new PageImpl<>(List.of(availablePet), pageable, 1)); - var result = petService.getAllPets(null, null, null, pageable); + var result = petService.getAllPets(null, null, null, null, pageable); assertEquals(1, result.getTotalElements()); assertEquals("Buddy", result.getContent().get(0).getPetName()); - verify(petRepository).searchPublicPets(null, null, pageable); - verify(petRepository, never()).searchPets(null, null, null, pageable); + verify(petRepository).searchPublicPets(null, null, null, pageable); + verify(petRepository, never()).searchPets(null, null, null, null, pageable); } @Test void getAllPetsAnonymousWithAdoptedStatusReturnsEmptyPage() { Pageable pageable = PageRequest.of(0, 10); - var result = petService.getAllPets(null, null, "Adopted", pageable); + var result = petService.getAllPets(null, null, "Adopted", null, pageable); assertEquals(0, result.getTotalElements()); - verify(petRepository, never()).searchPublicPets(null, null, pageable); + verify(petRepository, never()).searchPublicPets(null, null, null, pageable); } @Test @@ -83,7 +91,7 @@ class PetServiceTest { when(petRepository.searchCustomerVisiblePets(25L, null, null, null, pageable)) .thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2)); - var result = petService.getAllPets(null, null, null, pageable); + var result = petService.getAllPets(null, null, null, null, pageable); assertEquals(2, result.getTotalElements()); verify(petRepository).searchCustomerVisiblePets(25L, null, null, null, pageable); @@ -95,13 +103,13 @@ class PetServiceTest { setAuthentication(99L, User.Role.ADMIN); Pet availablePet = pet(1L, "Buddy", "Available"); Pet adoptedPet = pet(2L, "Luna", "Adopted"); - when(petRepository.searchPets(null, null, null, pageable)) + when(petRepository.searchPets(null, null, null, null, pageable)) .thenReturn(new PageImpl<>(List.of(availablePet, adoptedPet), pageable, 2)); - var result = petService.getAllPets(null, null, null, pageable); + var result = petService.getAllPets(null, null, null, null, pageable); assertEquals(2, result.getTotalElements()); - verify(petRepository).searchPets(null, null, null, pageable); + verify(petRepository).searchPets(null, null, null, null, pageable); } @Test -- 2.49.1 From b2f291f2569c333bfb88dbb3fe3e331d210c7d66 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 15:50:49 -0600 Subject: [PATCH 2/3] Update pet desktop --- .../api/dto/pet/PetRequest.java | 18 +++ .../api/dto/pet/PetResponse.java | 36 +++++ .../petshopdesktop/api/endpoints/PetApi.java | 11 +- .../controllers/PetController.java | 17 ++- .../PetDialogController.java | 139 +++++++++++++++++- .../example/petshopdesktop/models/Pet.java | 44 ++++++ .../dialogviews/pet-dialog-view.fxml | 30 +++- .../petshopdesktop/modelviews/pet-view.fxml | 2 + 8 files changed, 286 insertions(+), 11 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetRequest.java index be71c214..4bb56185 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetRequest.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetRequest.java @@ -9,6 +9,8 @@ public class PetRequest { private Integer petAge; private String petStatus; private BigDecimal petPrice; + private Long customerId; + private Long storeId; public PetRequest() { } @@ -60,4 +62,20 @@ public class PetRequest { public void setPetPrice(BigDecimal petPrice) { 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; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetResponse.java index b1155214..f0961a18 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetResponse.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetResponse.java @@ -14,6 +14,10 @@ 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() { } @@ -97,4 +101,36 @@ public class PetResponse { public void setUpdatedAt(LocalDateTime updatedAt) { 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; + } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java index 92fa28c9..5f69e025 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java @@ -24,7 +24,7 @@ public class PetApi { return INSTANCE; } - public List listPets(String query, String species, String status) throws Exception { + public List listPets(String query, String species, String status, Long storeId) throws Exception { String path = "/api/v1/pets?page=0&size=1000"; if (query != null && !query.isEmpty()) { path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); @@ -35,6 +35,9 @@ public class PetApi { if (status != null && !status.isEmpty()) { path += "&status=" + URLEncoder.encode(status, StandardCharsets.UTF_8); } + if (storeId != null) { + path += "&storeId=" + storeId; + } String response = apiClient.getRawResponse(path); PageResponse pageResponse = apiClient.getObjectMapper().readValue( response, @@ -46,8 +49,12 @@ public class PetApi { return pageResponse.getContent(); } + public List listPets(String query, String species, String status) throws Exception { + return listPets(query, species, status, null); + } + public List listPets(String query) throws Exception { - return listPets(query, null, null); + return listPets(query, null, null, null); } public PetResponse createPet(PetRequest request) throws Exception { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java index bd76c9ce..005eab1b 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java @@ -63,6 +63,12 @@ public class PetController { @FXML private TableColumn colPetStatus; + @FXML + private TableColumn colCustomerName; + + @FXML + private TableColumn colStoreName; + @FXML private TableView tvPets; @@ -156,11 +162,13 @@ public class PetController { colPetAge.setCellValueFactory(new PropertyValueFactory("petAge")); colPetStatus.setCellValueFactory(new PropertyValueFactory("petStatus")); colPetPrice.setCellValueFactory(new PropertyValueFactory("petPrice")); + colCustomerName.setCellValueFactory(new PropertyValueFactory("customerName")); + colStoreName.setCellValueFactory(new PropertyValueFactory("storeName")); configureImageColumn(colPetImage); loadSpeciesFilter(); - cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Pending")); + cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Owned", "Pending")); cbStatusFilter.getSelectionModel().selectFirst(); displayPets(); @@ -316,7 +324,7 @@ public class PetController { } private Pet mapToPet(PetResponse response) { - return new Pet( + Pet pet = new Pet( response.getPetId().intValue(), response.getPetName(), response.getPetSpecies(), @@ -326,6 +334,11 @@ public class PetController { response.getPetPrice().doubleValue(), response.getImageUrl() ); + pet.setCustomerName(response.getCustomerName()); + pet.setStoreName(response.getStoreName()); + pet.setCustomerId(response.getCustomerId() != null ? response.getCustomerId() : 0L); + pet.setStoreId(response.getStoreId() != null ? response.getStoreId() : 0L); + return pet; } private void configureImageColumn(TableColumn column) { diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java index 78ad836a..afd2f549 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java @@ -1,5 +1,6 @@ package org.example.petshopdesktop.controllers.dialogcontrollers; +import javafx.application.Platform; import javafx.collections.FXCollections; import javafx.collections.ObservableList; import javafx.event.EventHandler; @@ -10,8 +11,10 @@ import javafx.scene.image.ImageView; import javafx.scene.input.MouseEvent; import javafx.stage.Stage; import org.example.petshopdesktop.Validator; +import org.example.petshopdesktop.api.dto.common.DropdownOption; import org.example.petshopdesktop.api.dto.pet.PetRequest; import org.example.petshopdesktop.api.dto.pet.PetResponse; +import org.example.petshopdesktop.api.endpoints.DropdownApi; import org.example.petshopdesktop.api.endpoints.PetApi; import org.example.petshopdesktop.models.Pet; import org.example.petshopdesktop.util.ActivityLogger; @@ -20,6 +23,7 @@ import org.example.petshopdesktop.util.FilePickerSupport; import java.io.File; import java.math.BigDecimal; +import java.util.List; public class PetDialogController { @@ -38,6 +42,12 @@ public class PetDialogController { @FXML private ComboBox cbPetStatus; + @FXML + private ComboBox cbCustomer; + + @FXML + private ComboBox cbStore; + @FXML private Label lblMode; @@ -70,16 +80,54 @@ public class PetDialogController { private String currentImageUrl; private boolean removeImageRequested; + private Long pendingCustomerId = null; + private Long pendingStoreId = null; + private ObservableList statusList = FXCollections.observableArrayList( - "Available", "Adopted" + "Available", "Adopted", "Owned" ); @FXML void initialize() { - cbPetStatus.setItems(statusList); //set status combobox + cbPetStatus.setItems(statusList); + + cbCustomer.setCellFactory(param -> new ListCell<>() { + @Override protected void updateItem(DropdownOption o, boolean empty) { + super.updateItem(o, empty); + setText(empty || o == null ? null : o.getLabel()); + } + }); + cbCustomer.setButtonCell(new ListCell<>() { + @Override protected void updateItem(DropdownOption o, boolean empty) { + super.updateItem(o, empty); + setText(empty || o == null ? null : o.getLabel()); + } + }); + + cbStore.setCellFactory(param -> new ListCell<>() { + @Override protected void updateItem(DropdownOption o, boolean empty) { + super.updateItem(o, empty); + setText(empty || o == null ? null : o.getLabel()); + } + }); + cbStore.setButtonCell(new ListCell<>() { + @Override protected void updateItem(DropdownOption o, boolean empty) { + super.updateItem(o, empty); + setText(empty || o == null ? null : o.getLabel()); + } + }); + + cbCustomer.setVisible(false); + cbStore.setVisible(false); + + cbPetStatus.valueProperty().addListener((obs, oldVal, newVal) -> { + boolean isOwned = "Owned".equalsIgnoreCase(newVal); + boolean isAvailable = "Available".equalsIgnoreCase(newVal) || "Unadopted".equalsIgnoreCase(newVal); + cbCustomer.setVisible(isOwned); + cbStore.setVisible(isAvailable); + }); - //Set up mouse handlers for buttons btnSave.setOnMouseClicked(new EventHandler() { @Override public void handle(MouseEvent mouseEvent) { @@ -97,6 +145,9 @@ public class PetDialogController { btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage()); btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage()); refreshImagePreview(); + + loadCustomers(); + loadStores(); } private void buttonSaveClicked(MouseEvent mouseEvent) { @@ -111,6 +162,10 @@ public class PetDialogController { if (cbPetStatus.getSelectionModel().getSelectedItem() == null){ errorMsg += "Status is required"; } + String selectedStatus = cbPetStatus.getValue(); + if ("Owned".equalsIgnoreCase(selectedStatus) && cbCustomer.getValue() == null) { + errorMsg += "Customer is required for Owned status\n"; + } //Check validation (length size) errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50); @@ -184,9 +239,81 @@ public class PetDialogController { } request.setPetAge(age); + String status = cbPetStatus.getValue(); + if ("Owned".equalsIgnoreCase(status) && cbCustomer.getValue() != null) { + request.setCustomerId(cbCustomer.getValue().getId()); + } + if (("Available".equalsIgnoreCase(status) || "Unadopted".equalsIgnoreCase(status)) && cbStore.getValue() != null) { + request.setStoreId(cbStore.getValue().getId()); + } + return request; } + private void loadCustomers() { + new Thread(() -> { + try { + List customers = DropdownApi.getInstance().getCustomers(); + Platform.runLater(() -> { + cbCustomer.setItems(FXCollections.observableArrayList(customers)); + applySelectedCustomer(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "PetDialogController.loadCustomers", e, "Loading customers"); + cbCustomer.setDisable(true); + cbCustomer.setPromptText("Unable to load customers"); + }); + } + }).start(); + } + + private void loadStores() { + new Thread(() -> { + try { + List stores = DropdownApi.getInstance().getStores(); + Platform.runLater(() -> { + cbStore.setItems(FXCollections.observableArrayList(stores)); + applySelectedStore(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "PetDialogController.loadStores", e, "Loading stores"); + cbStore.setDisable(true); + cbStore.setPromptText("Unable to load stores"); + }); + } + }).start(); + } + + private void applySelectedCustomer() { + if (pendingCustomerId == null) return; + DropdownOption selected = findOptionById(cbCustomer.getItems(), pendingCustomerId); + if (selected != null) { + cbCustomer.setValue(selected); + pendingCustomerId = null; + } + } + + private void applySelectedStore() { + if (pendingStoreId == null) return; + DropdownOption selected = findOptionById(cbStore.getItems(), pendingStoreId); + if (selected != null) { + cbStore.setValue(selected); + pendingStoreId = null; + } + } + + private DropdownOption findOptionById(List options, Long id) { + if (id == null || options == null) return null; + for (DropdownOption option : options) { + if (option.getId() != null && option.getId().equals(id)) return option; + } + return null; + } + private void closeStage(MouseEvent mouseEvent) { Node node = (Node) mouseEvent.getSource(); Stage stage = (Stage) node.getScene().getWindow(); @@ -206,14 +333,14 @@ public class PetDialogController { removeImageRequested = false; refreshImagePreview(); - //get the right combobox selection + pendingCustomerId = pet.getCustomerId() > 0 ? pet.getCustomerId() : null; + pendingStoreId = pet.getStoreId() > 0 ? pet.getStoreId() : null; + for (String status : cbPetStatus.getItems()) { if(status.equals(pet.getPetStatus())){ cbPetStatus.getSelectionModel().select(status); } } - - } } diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java b/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java index fc1723c5..bc7e1a5c 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java @@ -13,6 +13,10 @@ public class Pet { private SimpleStringProperty petStatus; private SimpleDoubleProperty petPrice; private SimpleStringProperty imageUrl; + private SimpleStringProperty customerName = new SimpleStringProperty(""); + private SimpleStringProperty storeName = new SimpleStringProperty(""); + private long customerId = 0L; + private long storeId = 0L; public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice, String imageUrl) { this.petId = new SimpleIntegerProperty(petId); @@ -120,4 +124,44 @@ public class Pet { public SimpleStringProperty imageUrlProperty() { return imageUrl; } + + public String getCustomerName() { + return customerName.get(); + } + + public void setCustomerName(String customerName) { + this.customerName.set(customerName != null ? customerName : ""); + } + + public SimpleStringProperty customerNameProperty() { + return customerName; + } + + public String getStoreName() { + return storeName.get(); + } + + public void setStoreName(String storeName) { + this.storeName.set(storeName != null ? storeName : ""); + } + + public SimpleStringProperty storeNameProperty() { + return storeName; + } + + 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; + } } diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml index 2f5bd110..f25e113f 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml @@ -13,7 +13,7 @@ - + @@ -153,6 +153,34 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml index c8e77504..d599e010 100644 --- a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/pet-view.fxml @@ -79,6 +79,8 @@ + + -- 2.49.1 From 28e53a437977bc85af219c4055cff3851dda247a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Mon, 6 Apr 2026 15:50:52 -0600 Subject: [PATCH 3/3] Seed pets and appointments --- .../db/migration/V20__seed_owned_pets.sql | 5 + .../V21__bulk_seed_pets_and_appointments.sql | 144 ++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 backend/src/main/resources/db/migration/V20__seed_owned_pets.sql create mode 100644 backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql diff --git a/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql b/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql new file mode 100644 index 00000000..50a91e1b --- /dev/null +++ b/backend/src/main/resources/db/migration/V20__seed_owned_pets.sql @@ -0,0 +1,5 @@ +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) +VALUES + ('Pepper', 'Cat', 'Tabby', 3, 'Owned', 0.00, 1), + ('Coco', 'Dog', 'Pomeranian', 2, 'Owned', 0.00, 4), + ('Finn', 'Dog', 'Border Collie', 5, 'Owned', 0.00, 6); diff --git a/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql b/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql new file mode 100644 index 00000000..441351ab --- /dev/null +++ b/backend/src/main/resources/db/migration/V21__bulk_seed_pets_and_appointments.sql @@ -0,0 +1,144 @@ +INSERT INTO customer (firstName, lastName, email) VALUES +('Noah', 'Parker', 'noah@gmail.com'), +('Mia', 'Evans', 'mia@gmail.com'), +('Ethan', 'Scott', 'ethan@gmail.com'), +('Chloe', 'Adams', 'chloe@gmail.com'), +('Lucas', 'Baker', 'lucas@gmail.com'), +('Lily', 'Hall', 'lily@gmail.com'), +('Mason', 'Rivera', 'mason@gmail.com'), +('Ella', 'Mitchell', 'ella@gmail.com'), +('James', 'Carter', 'jcarter@gmail.com'), +('Harper', 'Collins', 'harper@gmail.com'); + +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, storeId) VALUES +('Rocky', 'Dog', 'German Shepherd', 1, 'Available', 475.00, 1), +('Daisy', 'Dog', 'Poodle', 2, 'Available', 512.00, 1), +('Cooper', 'Dog', 'Bulldog', 3, 'Available', 560.00, 1), +('Ruby', 'Dog', 'Boxer', 4, 'Available', 575.00, 1), +('Tucker', 'Dog', 'Dachshund', 5, 'Available', 634.00, 1), +('Rosie', 'Dog', 'Shih Tzu', 1, 'Available', 660.00, 2), +('Bear', 'Dog', 'Rottweiler', 2, 'Available', 686.00, 2), +('Maggie', 'Dog', 'Corgi', 3, 'Available', 745.00, 2), +('Leo', 'Dog', 'Husky', 4, 'Available', 749.00, 2), +('Zoey', 'Cat', 'Ragdoll', 1, 'Available', 420.00, 1), +('Oliver', 'Cat', 'British Shorthair', 2, 'Available', 395.00, 1), +('Lola', 'Cat', 'Bengal', 3, 'Available', 465.00, 3), +('Buster', 'Dog', 'Beagle', 2, 'Available', 440.00, 3), +('Sadie', 'Dog', 'Golden Retriever', 1, 'Available', 535.00, 3), +('Toby', 'Dog', 'Labrador', 5, 'Available', 490.00, 1), +('Cleo', 'Cat', 'Abyssinian', 2, 'Available', 375.00, 2), +('Harley', 'Dog', 'Dalmatian', 3, 'Available', 520.00, 1), +('Mocha', 'Cat', 'Burmese', 1, 'Available', 345.00, 3), +('Rex', 'Dog', 'Doberman', 4, 'Available', 610.00, 1), +('Willow', 'Cat', 'Scottish Fold', 2, 'Available', 480.00, 2), +('Gizmo', 'Dog', 'Pomeranian', 1, 'Available', 530.00, 1), +('Nala', 'Cat', 'Siamese', 3, 'Available', 360.00, 2), +('Duke', 'Dog', 'Great Dane', 2, 'Available', 720.00, 3), +('Misty', 'Cat', 'Russian Blue', 4, 'Available', 410.00, 1), +('Ace', 'Dog', 'Australian Shepherd', 1, 'Available', 555.00, 1); + +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) VALUES +('Shadow', 'Dog', 'Labrador', 3, 'Adopted', 500.00, 1), +('Kitty', 'Cat', 'Persian', 2, 'Adopted', 320.00, 2), +('Bruno', 'Dog', 'Rottweiler', 4, 'Adopted', 580.00, 3), +('Snowball', 'Cat', 'Turkish Angora', 1, 'Adopted', 390.00, 4), +('Zeus', 'Dog', 'Husky', 3, 'Adopted', 640.00, 5); + +INSERT INTO pet (petName, petSpecies, petBreed, petAge, petStatus, petPrice, customerId) VALUES +('Biscuit', 'Dog', 'Beagle', 2, 'Owned', 0.00, 6), +('Patches', 'Cat', 'Calico', 5, 'Owned', 0.00, 7), +('Scout', 'Dog', 'Border Collie', 3, 'Owned', 0.00, 8), +('Mittens', 'Cat', 'Domestic Short', 4, 'Owned', 0.00, 9), +('Thor', 'Dog', 'German Shepherd', 2, 'Owned', 0.00, 10); + +INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) +SELECT p.petId, p.customerId, + (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), + '2026-01-10', 'Completed' +FROM pet p WHERE p.petName = 'Shadow' AND p.petStatus = 'Adopted'; + +INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) +SELECT p.petId, p.customerId, + (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), + '2026-01-18', 'Completed' +FROM pet p WHERE p.petName = 'Kitty' AND p.petStatus = 'Adopted'; + +INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) +SELECT p.petId, p.customerId, + (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), + '2026-02-03', 'Completed' +FROM pet p WHERE p.petName = 'Bruno' AND p.petStatus = 'Adopted'; + +INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) +SELECT p.petId, p.customerId, + (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), + '2026-02-14', 'Completed' +FROM pet p WHERE p.petName = 'Snowball' AND p.petStatus = 'Adopted'; + +INSERT INTO adoption (petId, customerId, employeeId, adoptionDate, adoptionStatus) +SELECT p.petId, p.customerId, + (SELECT e.employeeId FROM employee e JOIN users u ON u.id = e.user_id + WHERE e.isActive = TRUE AND u.role = 'STAFF' ORDER BY e.employeeId LIMIT 1), + '2026-02-21', 'Completed' +FROM pet p WHERE p.petName = 'Zeus' AND p.petStatus = 'Adopted'; + +INSERT INTO customer_pet (customer_id, pet_name, species, breed) VALUES +(1, 'Rex', 'Dog', 'German Shepherd'), +(2, 'Whiskers', 'Cat', 'Tabby'), +(3, 'Goldie', 'Dog', 'Golden Retriever'), +(4, 'Midnight', 'Cat', 'Black'), +(5, 'Storm', 'Dog', 'Husky'), +(6, 'Peanut', 'Dog', 'Poodle'), +(7, 'Snowball', 'Cat', 'Persian'), +(8, 'Duke', 'Dog', 'Labrador'), +(9, 'Luna', 'Cat', 'Siamese'), +(10, 'Buster', 'Dog', 'Beagle'), +(11, 'Daisy', 'Dog', 'Corgi'), +(12, 'Cleo', 'Cat', 'Ragdoll'); + +INSERT INTO appointment (serviceId, customerId, appointmentDate, appointmentTime, appointmentStatus, storeId, employeeId) VALUES +(1, 1, '2026-01-10', '09:00:00', 'Completed', 1, 1), +(2, 2, '2026-01-10', '11:00:00', 'Completed', 1, 1), +(3, 3, '2026-01-17', '09:00:00', 'Missed', 1, 1), +(4, 4, '2026-01-17', '14:00:00', 'Completed', 1, 1), +(5, 5, '2026-01-24', '10:00:00', 'Completed', 1, 1), +(1, 6, '2026-01-24', '13:00:00', 'Missed', 1, 1), +(2, 7, '2026-02-07', '09:00:00', 'Completed', 1, 1), +(3, 8, '2026-02-07', '11:00:00', 'Completed', 1, 1), +(1, 9, '2026-01-11', '09:00:00', 'Completed', 1, 2), +(2, 10, '2026-01-11', '11:00:00', 'Missed', 1, 2), +(3, 11, '2026-01-18', '10:00:00', 'Completed', 1, 2), +(4, 12, '2026-01-18', '13:00:00', 'Completed', 1, 2), +(5, 1, '2026-02-01', '09:00:00', 'Completed', 1, 2), +(1, 2, '2026-02-01', '14:00:00', 'Missed', 1, 2), +(2, 3, '2026-02-08', '10:00:00', 'Completed', 1, 2), +(3, 4, '2026-02-08', '13:00:00', 'Completed', 1, 2), +(4, 5, '2026-01-12', '09:00:00', 'Completed', 1, 5), +(5, 6, '2026-01-12', '11:00:00', 'Completed', 1, 5), +(1, 7, '2026-01-19', '09:00:00', 'Missed', 1, 5), +(2, 8, '2026-01-19', '14:00:00', 'Completed', 1, 5), +(3, 9, '2026-02-09', '10:00:00', 'Completed', 1, 5), +(4, 10, '2026-02-09', '13:00:00', 'Completed', 1, 5), +(1, 11, '2026-01-13', '09:00:00', 'Completed', 2, 3), +(2, 12, '2026-01-13', '11:00:00', 'Completed', 2, 3), +(3, 1, '2026-02-10', '09:00:00', 'Missed', 2, 3), +(4, 2, '2026-02-10', '13:00:00', 'Completed', 2, 3), +(1, 3, '2026-01-14', '10:00:00', 'Completed', 3, 4), +(2, 4, '2026-01-14', '13:00:00', 'Completed', 3, 4), +(3, 5, '2026-02-11', '10:00:00', 'Missed', 3, 4), +(4, 6, '2026-02-11', '14:00:00', 'Completed', 3, 4), +(1, 7, '2026-04-15', '09:00:00', 'Booked', 1, 1), +(2, 8, '2026-04-15', '11:00:00', 'Booked', 1, 2), +(3, 9, '2026-04-16', '10:00:00', 'Booked', 1, 5), +(4, 10, '2026-04-17', '09:00:00', 'Booked', 2, 3), +(5, 11, '2026-04-18', '14:00:00', 'Booked', 3, 4); + +INSERT INTO appointment_customer_pet (appointment_id, customer_pet_id) +SELECT a.appointmentId, + (((a.appointmentId - 6) % 12) + 1) +FROM appointment a +WHERE a.appointmentId BETWEEN 6 AND 40; -- 2.49.1