Fix appointment ownership

This commit is contained in:
2026-04-02 10:10:04 -06:00
parent ca06f6c8b3
commit 259770ce69
9 changed files with 226 additions and 26 deletions

View File

@@ -1,10 +1,12 @@
package com.petshop.backend.controller;
import com.petshop.backend.dto.common.DropdownOption;
import com.petshop.backend.entity.CustomerPet;
import com.petshop.backend.repository.*;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -17,6 +19,7 @@ public class DropdownController {
private final PetRepository petRepository;
private final CustomerRepository customerRepository;
private final CustomerPetRepository customerPetRepository;
private final ServiceRepository serviceRepository;
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
@@ -24,11 +27,13 @@ public class DropdownController {
private final SupplierRepository supplierRepository;
public DropdownController(PetRepository petRepository, CustomerRepository customerRepository,
ServiceRepository serviceRepository, ProductRepository productRepository,
CategoryRepository categoryRepository, StoreRepository storeRepository,
SupplierRepository supplierRepository) {
CustomerPetRepository customerPetRepository,
ServiceRepository serviceRepository, ProductRepository productRepository,
CategoryRepository categoryRepository, StoreRepository storeRepository,
SupplierRepository supplierRepository) {
this.petRepository = petRepository;
this.customerRepository = customerRepository;
this.customerPetRepository = customerPetRepository;
this.serviceRepository = serviceRepository;
this.productRepository = productRepository;
this.categoryRepository = categoryRepository;
@@ -55,6 +60,16 @@ public class DropdownController {
);
}
@GetMapping("/customers/{customerId}/pets")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<List<DropdownOption>> getCustomerPets(@PathVariable Long customerId) {
return ResponseEntity.ok(
customerPetRepository.findByCustomerCustomerIdOrderByPetNameAsc(customerId).stream()
.map(this::toCustomerPetOption)
.collect(Collectors.toList())
);
}
@GetMapping("/services")
public ResponseEntity<List<DropdownOption>> getServices() {
return ResponseEntity.ok(
@@ -123,4 +138,10 @@ public class DropdownController {
.collect(Collectors.toList())
);
}
private DropdownOption toCustomerPetOption(CustomerPet pet) {
String species = pet.getSpecies() == null || pet.getSpecies().isBlank() ? "Pet" : pet.getSpecies();
String breed = pet.getBreed() == null || pet.getBreed().isBlank() ? "" : " · " + pet.getBreed();
return new DropdownOption(pet.getCustomerPetId(), pet.getPetName() + " (" + species + breed + ")");
}
}

View File

@@ -12,5 +12,7 @@ public interface CustomerPetRepository extends JpaRepository<CustomerPet, Long>
List<CustomerPet> findByCustomerCustomerIdOrderByCreatedAtDesc(Long customerId);
List<CustomerPet> findByCustomerCustomerIdOrderByPetNameAsc(Long customerId);
Optional<CustomerPet> findByCustomerPetIdAndCustomerCustomerId(Long customerPetId, Long customerId);
}

View File

@@ -120,7 +120,7 @@ public class AppointmentService {
}
Set<Pet> pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>();
Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>();
Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>();
Appointment appointment = new Appointment();
appointment.setCustomer(customer);
@@ -164,7 +164,7 @@ public class AppointmentService {
}
Set<Pet> pets = hasPetIds ? fetchPets(request.getPetIds()) : new HashSet<>();
Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds()) : new HashSet<>();
Set<CustomerPet> customerPets = hasCustomerPetIds ? fetchCustomerPets(request.getCustomerPetIds(), customer.getCustomerId()) : new HashSet<>();
appointment.setCustomer(customer);
appointment.setStore(store);
@@ -247,11 +247,14 @@ public class AppointmentService {
return pets;
}
private Set<CustomerPet> fetchCustomerPets(List<Long> customerPetIds) {
private Set<CustomerPet> fetchCustomerPets(List<Long> customerPetIds, Long customerId) {
Set<CustomerPet> customerPets = new HashSet<>();
for (Long customerPetId : customerPetIds) {
CustomerPet customerPet = customerPetRepository.findById(customerPetId)
.orElseThrow(() -> new ResourceNotFoundException("Customer pet not found with id: " + customerPetId));
if (!customerPet.getCustomer().getCustomerId().equals(customerId)) {
throw new IllegalArgumentException("Selected pet does not belong to the selected customer");
}
customerPets.add(customerPet);
}

View File

@@ -2,12 +2,16 @@ package com.petshop.backend.service;
import com.petshop.backend.entity.Appointment;
import com.petshop.backend.entity.Customer;
import com.petshop.backend.entity.CustomerPet;
import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.Service;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.AppointmentRepository;
import com.petshop.backend.repository.CustomerPetRepository;
import com.petshop.backend.repository.CustomerRepository;
import com.petshop.backend.repository.EmployeeRepository;
import com.petshop.backend.repository.EmployeeStoreRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.ServiceRepository;
import com.petshop.backend.repository.StoreRepository;
@@ -29,6 +33,7 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -44,6 +49,9 @@ class AppointmentServiceTest {
@Mock
private CustomerRepository customerRepository;
@Mock
private CustomerPetRepository customerPetRepository;
@Mock
private PetRepository petRepository;
@@ -56,6 +64,12 @@ class AppointmentServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmployeeRepository employeeRepository;
@Mock
private EmployeeStoreRepository employeeStoreRepository;
@InjectMocks
private AppointmentService appointmentService;
@@ -64,6 +78,7 @@ class AppointmentServiceTest {
private Service grooming;
private Service nailTrim;
private Pet pet;
private CustomerPet customerPet;
private LocalDate date;
@BeforeEach
@@ -91,6 +106,11 @@ class AppointmentServiceTest {
pet.setPetId(1L);
pet.setPetName("Milo");
customerPet = new CustomerPet();
customerPet.setCustomerPetId(11L);
customerPet.setPetName("Milo Jr");
customerPet.setCustomer(customer);
date = LocalDate.now().plusDays(1);
}
@@ -144,14 +164,7 @@ class AppointmentServiceTest {
user.setRole(User.Role.CUSTOMER);
user.setTokenVersion(0);
when(userRepository.findById(10L)).thenReturn(Optional.of(user));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
new com.petshop.backend.security.AppPrincipal(10L, "pat", User.Role.CUSTOMER, 0),
"n/a",
List.of(new SimpleGrantedAuthority("ROLE_CUSTOMER"))
)
);
setAuthentication(10L, User.Role.CUSTOMER);
when(appointmentRepository.findById(1L)).thenReturn(Optional.of(existing));
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
@@ -176,6 +189,78 @@ class AppointmentServiceTest {
assertEquals("Booked", response.getAppointmentStatus());
}
@Test
void createAppointmentRejectsCustomerPetOwnedByDifferentCustomer() {
User adminUser = new User();
adminUser.setId(99L);
adminUser.setUsername("admin");
adminUser.setRole(User.Role.ADMIN);
adminUser.setTokenVersion(0);
when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser));
setAuthentication(99L, User.Role.ADMIN);
Customer otherCustomer = new Customer();
otherCustomer.setCustomerId(2L);
CustomerPet otherCustomerPet = new CustomerPet();
otherCustomerPet.setCustomerPetId(22L);
otherCustomerPet.setCustomer(otherCustomer);
otherCustomerPet.setPetName("Not Yours");
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of());
when(customerPetRepository.findById(22L)).thenReturn(Optional.of(otherCustomerPet));
var request = new com.petshop.backend.dto.appointment.AppointmentRequest();
request.setCustomerId(1L);
request.setStoreId(1L);
request.setServiceId(1L);
request.setAppointmentDate(date);
request.setAppointmentTime(LocalTime.of(10, 0));
request.setAppointmentStatus("Booked");
request.setCustomerPetIds(List.of(22L));
assertThrows(IllegalArgumentException.class, () -> appointmentService.createAppointment(request));
}
@Test
void createAppointmentAllowsCustomerOwnedCustomerPet() {
User adminUser = new User();
adminUser.setId(99L);
adminUser.setUsername("admin");
adminUser.setRole(User.Role.ADMIN);
adminUser.setTokenVersion(0);
when(userRepository.findById(99L)).thenReturn(Optional.of(adminUser));
setAuthentication(99L, User.Role.ADMIN);
when(customerRepository.findById(1L)).thenReturn(Optional.of(customer));
when(storeRepository.findById(1L)).thenReturn(Optional.of(store));
when(serviceRepository.findById(1L)).thenReturn(Optional.of(grooming));
when(appointmentRepository.findByStoreAndDate(1L, date)).thenReturn(List.of());
when(customerPetRepository.findById(11L)).thenReturn(Optional.of(customerPet));
when(appointmentRepository.save(any(Appointment.class))).thenAnswer(invocation -> {
Appointment appointment = invocation.getArgument(0);
appointment.setAppointmentId(99L);
return appointment;
});
var request = new com.petshop.backend.dto.appointment.AppointmentRequest();
request.setCustomerId(1L);
request.setStoreId(1L);
request.setServiceId(1L);
request.setAppointmentDate(date);
request.setAppointmentTime(LocalTime.of(10, 0));
request.setAppointmentStatus("Booked");
request.setCustomerPetIds(List.of(11L));
var response = appointmentService.createAppointment(request);
assertEquals(99L, response.getAppointmentId());
assertEquals(1L, response.getCustomerId());
}
private Appointment appointment(Long id, LocalDate date, LocalTime time, Service service, StoreLocation storeLocation) {
Appointment appointment = new Appointment();
appointment.setAppointmentId(id);
@@ -188,4 +273,14 @@ class AppointmentServiceTest {
appointment.setPets(Set.of());
return appointment;
}
private void setAuthentication(Long userId, User.Role role) {
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(
new com.petshop.backend.security.AppPrincipal(userId, "user", role, 0),
"n/a",
List.of(new SimpleGrantedAuthority("ROLE_" + role.name()))
)
);
}
}