Fixes for appointments and My Pets fields.

This commit is contained in:
augmentedpotato
2026-04-14 12:20:48 -06:00
parent 208372c782
commit c2f39c40f0
13 changed files with 570 additions and 193 deletions

View File

@@ -87,10 +87,28 @@ public class AdoptionController {
public ResponseEntity<AdoptionResponse> requestAdoption(@Valid @RequestBody CustomerAdoptionRequest request) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
return ResponseEntity.status(HttpStatus.CREATED).body(
adoptionService.requestAdoption(user.getId(), request.getPetId(), request.getEmployeeId(), request.getSourceStoreId())
adoptionService.requestAdoption(user.getId(), request.getPetId(), request.getEmployeeId(), request.getSourceStoreId(), request.getAdoptionDate())
);
}
@PatchMapping("/{id}/cancel")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<AdoptionResponse> cancelAdoption(@PathVariable Long id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = null;
if ("CUSTOMER".equals(role)) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
customerId = user.getId();
}
return ResponseEntity.ok(adoptionService.cancelAdoption(id, customerId));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<AdoptionResponse> updateAdoption(

View File

@@ -98,6 +98,24 @@ public class AppointmentController {
return ResponseEntity.status(HttpStatus.CREATED).body(appointmentService.createAppointment(request));
}
@PatchMapping("/{id}/cancel")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<AppointmentResponse> cancelAppointment(@PathVariable Long id) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long customerId = null;
if ("CUSTOMER".equals(role)) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
customerId = user.getId();
}
return ResponseEntity.ok(appointmentService.cancelAppointment(id, customerId));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
public ResponseEntity<AppointmentResponse> updateAppointment(

View File

@@ -38,8 +38,8 @@ public class MyPetController {
}
@GetMapping
public ResponseEntity<List<MyPetResponse>> getMyPets() {
return ResponseEntity.ok(petService.getMyPets(currentUserId()));
public ResponseEntity<List<MyPetResponse>> getMyPets(@RequestParam(required = false) String status) {
return ResponseEntity.ok(petService.getMyPets(currentUserId(), status));
}
@PostMapping

View File

@@ -1,6 +1,7 @@
package com.petshop.backend.dto.adoption;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
public class CustomerAdoptionRequest {
@@ -11,6 +12,9 @@ public class CustomerAdoptionRequest {
private Long sourceStoreId;
@NotNull(message = "Appointment date is required")
private LocalDate adoptionDate;
public Long getPetId() {
return petId;
}
@@ -34,4 +38,12 @@ public class CustomerAdoptionRequest {
public void setSourceStoreId(Long sourceStoreId) {
this.sourceStoreId = sourceStoreId;
}
public LocalDate getAdoptionDate() {
return adoptionDate;
}
public void setAdoptionDate(LocalDate adoptionDate) {
this.adoptionDate = adoptionDate;
}
}

View File

@@ -7,16 +7,18 @@ public class MyPetResponse {
private String species;
private String breed;
private String imageUrl;
private String petStatus;
public MyPetResponse() {
}
public MyPetResponse(Long customerPetId, String petName, String species, String breed, String imageUrl) {
public MyPetResponse(Long customerPetId, String petName, String species, String breed, String imageUrl, String petStatus) {
this.customerPetId = customerPetId;
this.petName = petName;
this.species = species;
this.breed = breed;
this.imageUrl = imageUrl;
this.petStatus = petStatus;
}
public Long getCustomerPetId() {
@@ -58,4 +60,12 @@ public class MyPetResponse {
public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getPetStatus() {
return petStatus;
}
public void setPetStatus(String petStatus) {
this.petStatus = petStatus;
}
}

View File

@@ -50,4 +50,6 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
@Query("SELECT a FROM Appointment a WHERE (a.appointmentDate < :currentDate OR (a.appointmentDate = :currentDate AND a.appointmentTime < :currentTime)) AND LOWER(a.appointmentStatus) = 'booked'")
List<Appointment> findPastBookedAppointments(@Param("currentDate") LocalDate currentDate, @Param("currentTime") LocalTime currentTime);
List<Appointment> findByPet_Id(Long petId);
}

View File

@@ -97,7 +97,7 @@ public class SecurityConfig {
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("http://localhost:*", "http://127.0.0.1:*"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

View File

@@ -148,25 +148,30 @@ public class AdoptionService {
}
@Transactional
public AdoptionResponse requestAdoption(Long customerId, Long petId, Long employeeId, Long sourceStoreId) {
public AdoptionResponse requestAdoption(Long customerId, Long petId, Long employeeId, Long sourceStoreId, LocalDate adoptionDate) {
Pet pet = petRepository.findById(petId)
.orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + petId));
User customer = userRepository.findById(customerId)
.orElseThrow(() -> new ResourceNotFoundException("Customer not found with id: " + customerId));
User employee = resolveAdoptionEmployee(employeeId);
StoreLocation sourceStore = sourceStoreId != null
? storeRepository.findById(sourceStoreId)
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + sourceStoreId))
: null;
.orElseThrow(() -> new ResourceNotFoundException("Pet not found"));
// Verify the pet is actually located at the claimed store
if (pet.getStore() == null || !pet.getStore().getStoreId().equals(sourceStoreId)) {
throw new IllegalArgumentException("The specified pet is not located at the selected store.");
}
// Verify the pet is available for adoption
validatePetAvailability(pet, null, null);
User customer = userRepository.findById(customerId)
.orElseThrow(() -> new ResourceNotFoundException("Customer not found"));
User employee = resolveAdoptionEmployee(employeeId);
StoreLocation sourceStore = storeRepository.findById(sourceStoreId)
.orElseThrow(() -> new ResourceNotFoundException("Store not found"));
Adoption adoption = new Adoption();
adoption.setPet(pet);
adoption.setCustomer(customer);
adoption.setEmployee(employee);
adoption.setSourceStore(sourceStore);
adoption.setAdoptionDate(null);
adoption.setAdoptionDate(adoptionDate);
adoption.setAdoptionStatus(ADOPTION_STATUS_PENDING);
adoption = adoptionRepository.save(adoption);
@@ -174,6 +179,25 @@ public class AdoptionService {
return mapToResponse(adoption);
}
@Transactional
public AdoptionResponse cancelAdoption(Long adoptionId, Long requestingCustomerId) {
Adoption adoption = adoptionRepository.findById(adoptionId)
.orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + adoptionId));
if (requestingCustomerId != null && !adoption.getCustomer().getId().equals(requestingCustomerId)) {
throw new ResourceNotFoundException("Adoption not found");
}
if (!ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoption.getAdoptionStatus())) {
throw new IllegalArgumentException("Only pending adoptions can be cancelled");
}
adoption.setAdoptionStatus(ADOPTION_STATUS_CANCELLED);
adoption = adoptionRepository.save(adoption);
syncPetStatus(adoption.getPet(), ADOPTION_STATUS_CANCELLED, adoption.getAdoptionId(), adoption.getCustomer());
return mapToResponse(adoption);
}
@Transactional
public void deleteAdoption(Long id) {
if (!adoptionRepository.existsById(id)) {

View File

@@ -101,6 +101,22 @@ public class AppointmentService {
Pet pet = request.getPetId() != null ? fetchPet(request.getPetId()) : null;
User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
// Customers must supply a pet that is Adopted and owned by them
if (User.Role.CUSTOMER.equals(authenticatedUser.getRole())) {
if (pet == null) {
throw new IllegalArgumentException("A pet must be selected for your appointment");
}
if (pet.getOwner() == null || !pet.getOwner().getId().equals(authenticatedUser.getId())) {
throw new IllegalArgumentException("The selected pet does not belong to your account");
}
String petStatus = pet.getPetStatus();
if (!"Owned".equalsIgnoreCase(petStatus) && !"Adopted".equalsIgnoreCase(petStatus)) {
throw new IllegalArgumentException("Only your own pets can be booked for appointments");
}
}
validateSpeciesServiceCompatibility(pet, service);
validateStoreAccess(store.getStoreId(), authenticatedUser);
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
@@ -155,6 +171,23 @@ public class AppointmentService {
return mapToResponse(appointment);
}
@Transactional
public AppointmentResponse cancelAppointment(Long appointmentId, Long requestingCustomerId) {
Appointment appointment = appointmentRepository.findById(appointmentId)
.orElseThrow(() -> new ResourceNotFoundException("Appointment not found with id: " + appointmentId));
if (requestingCustomerId != null && !appointment.getCustomer().getId().equals(requestingCustomerId)) {
throw new ResourceNotFoundException("Appointment not found");
}
if (!"Booked".equalsIgnoreCase(appointment.getAppointmentStatus())) {
throw new IllegalArgumentException("Only booked appointments can be cancelled");
}
appointment.setAppointmentStatus("Cancelled");
return mapToResponse(appointmentRepository.save(appointment));
}
@Transactional
public void deleteAppointment(Long id) {
if (!appointmentRepository.existsById(id)) {
@@ -313,6 +346,32 @@ public class AppointmentService {
return true;
}
private void validateSpeciesServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) {
if (pet == null || service == null) return;
String species = pet.getPetSpecies();
if (species == null) return;
String serviceName = service.getServiceName().toLowerCase();
switch (species.toLowerCase()) {
case "bird":
if (!serviceName.contains("wing clipping") && !serviceName.contains("beak and nail")) {
throw new IllegalArgumentException(
"Service '" + service.getServiceName() + "' is not available for birds. " +
"Allowed services: Wing Clipping, Beak and Nail Care.");
}
break;
case "fish":
if (!serviceName.contains("aquarium health")) {
throw new IllegalArgumentException(
"Service '" + service.getServiceName() + "' is not available for fish. " +
"Allowed service: Aquarium Health Check.");
}
break;
default:
break;
}
}
private void validateStoreAccess(Long requestedStoreId, User user) {
if (user.getRole() != User.Role.STAFF) {
return;

View File

@@ -12,6 +12,7 @@ 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.AppointmentRepository;
import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository;
@@ -35,13 +36,15 @@ public class PetService {
private final PetRepository petRepository;
private final AdoptionRepository adoptionRepository;
private final AppointmentRepository appointmentRepository;
private final UserRepository userRepository;
private final StoreRepository storeRepository;
private final CatalogImageStorageService catalogImageStorageService;
public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, UserRepository userRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) {
public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, AppointmentRepository appointmentRepository, UserRepository userRepository, StoreRepository storeRepository, CatalogImageStorageService catalogImageStorageService) {
this.petRepository = petRepository;
this.adoptionRepository = adoptionRepository;
this.appointmentRepository = appointmentRepository;
this.userRepository = userRepository;
this.storeRepository = storeRepository;
this.catalogImageStorageService = catalogImageStorageService;
@@ -87,8 +90,9 @@ public class PetService {
}
@Transactional(readOnly = true)
public List<MyPetResponse> getMyPets(Long ownerUserId) {
public List<MyPetResponse> getMyPets(Long ownerUserId, String status) {
return petRepository.findAllByOwner_IdOrderByPetNameAsc(ownerUserId).stream()
.filter(p -> status == null || status.isBlank() || status.equalsIgnoreCase(p.getPetStatus()))
.map(this::mapToMyPetResponse)
.toList();
}
@@ -117,6 +121,18 @@ public class PetService {
@Transactional
public void deleteMyPet(Long ownerUserId, Long petId) {
Pet pet = findOwnedPet(ownerUserId, petId);
List<com.petshop.backend.entity.Appointment> linkedAppointments = appointmentRepository.findByPet_Id(petId);
boolean hasBooked = linkedAppointments.stream()
.anyMatch(a -> "Booked".equalsIgnoreCase(a.getAppointmentStatus()));
if (hasBooked) {
throw new IllegalArgumentException(
"Your pet has a booked appointment. Please cancel the appointment before removing your pet from our database.");
}
// Nullify the pet reference on non-booked appointments to avoid FK constraint violations
for (com.petshop.backend.entity.Appointment appt : linkedAppointments) {
appt.setPet(null);
}
appointmentRepository.saveAll(linkedAppointments);
deleteStoredImageIfPresent(pet.getImageUrl());
petRepository.delete(pet);
}
@@ -341,7 +357,8 @@ public class PetService {
pet.getPetName(),
pet.getPetSpecies(),
pet.getPetBreed(),
pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null
pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null,
pet.getPetStatus()
);
}