Merge remote-tracking branch 'origin/main' into azure-deploy

This commit is contained in:
2026-04-14 23:32:16 -06:00
46 changed files with 1616 additions and 333 deletions

View File

@@ -2,6 +2,7 @@ package com.petshop.backend.controller;
import com.petshop.backend.dto.adoption.AdoptionRequest;
import com.petshop.backend.dto.adoption.AdoptionResponse;
import com.petshop.backend.dto.adoption.CustomerAdoptionRequest;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
@@ -81,6 +82,33 @@ public class AdoptionController {
return ResponseEntity.status(HttpStatus.CREATED).body(adoptionService.createAdoption(request));
}
@PostMapping("/request")
@PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')")
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(), 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

@@ -73,7 +73,8 @@ public class AuthController {
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) {
String username = trimToNull(request.getUsername());
String email = trimToNull(request.getEmail());
NameParts nameParts = splitFullName(request.getFullName());
String firstName = trimToNull(request.getFirstName());
String lastName = trimToNull(request.getLastName());
String phone = normalizePhone(request.getPhone());
if (userRepository.findByUsername(username).isPresent()) {
@@ -101,9 +102,9 @@ public class AuthController {
user.setUsername(username);
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setEmail(email);
user.setFirstName(nameParts.firstName());
user.setLastName(nameParts.lastName());
user.setFullName(nameParts.fullName());
user.setFirstName(firstName);
user.setLastName(lastName);
user.setFullName(joinFullName(firstName, lastName));
user.setPhone(phone);
user.setRole(User.Role.CUSTOMER);
user.setActive(true);
@@ -208,11 +209,16 @@ public class AuthController {
user.setEmail(email);
}
if (request.getFullName() != null) {
NameParts nameParts = splitFullName(request.getFullName());
user.setFirstName(nameParts.firstName());
user.setLastName(nameParts.lastName());
user.setFullName(nameParts.fullName());
String firstName = trimToNull(request.getFirstName());
if (firstName != null) {
user.setFirstName(firstName);
}
String lastName = trimToNull(request.getLastName());
if (lastName != null) {
user.setLastName(lastName);
}
if (firstName != null || lastName != null) {
user.setFullName(joinFullName(user.getFirstName(), user.getLastName()));
}
if (request.getPhone() != null) {
@@ -252,6 +258,8 @@ public class AuthController {
return new UserInfoResponse(
user.getId(),
user.getUsername(),
user.getFirstName(),
user.getLastName(),
user.getEmail(),
fullName,
user.getPhone(),

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

@@ -23,7 +23,6 @@ public class StoreController {
}
@GetMapping
@PreAuthorize("isAuthenticated()")
public ResponseEntity<Page<StoreResponse>> getAllStores(
@RequestParam(required = false) String q,
Pageable pageable) {
@@ -31,7 +30,6 @@ public class StoreController {
}
@GetMapping("/{id}")
@PreAuthorize("isAuthenticated()")
public ResponseEntity<StoreResponse> getStoreById(@PathVariable Long id) {
return ResponseEntity.ok(storeService.getStoreById(id));
}

View File

@@ -0,0 +1,49 @@
package com.petshop.backend.dto.adoption;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
public class CustomerAdoptionRequest {
@NotNull(message = "Pet ID is required")
private Long petId;
private Long employeeId;
private Long sourceStoreId;
@NotNull(message = "Appointment date is required")
private LocalDate adoptionDate;
public Long getPetId() {
return petId;
}
public void setPetId(Long petId) {
this.petId = petId;
}
public Long getEmployeeId() {
return employeeId;
}
public void setEmployeeId(Long employeeId) {
this.employeeId = employeeId;
}
public Long getSourceStoreId() {
return sourceStoreId;
}
public void setSourceStoreId(Long sourceStoreId) {
this.sourceStoreId = sourceStoreId;
}
public LocalDate getAdoptionDate() {
return adoptionDate;
}
public void setAdoptionDate(LocalDate adoptionDate) {
this.adoptionDate = adoptionDate;
}
}

View File

@@ -11,8 +11,11 @@ public class ProfileUpdateRequest {
@Email(message = "Email must be valid")
private String email;
@Size(max = 100, message = "Full name must not exceed 100 characters")
private String fullName;
@Size(max = 50, message = "First name must not exceed 50 characters")
private String firstName;
@Size(max = 50, message = "Last name must not exceed 50 characters")
private String lastName;
@Size(max = 20, message = "Phone must not exceed 20 characters")
private String phone;
@@ -36,12 +39,20 @@ public class ProfileUpdateRequest {
this.email = email;
}
public String getFullName() {
return fullName;
public String getFirstName() {
return firstName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getPhone() {
@@ -67,14 +78,15 @@ public class ProfileUpdateRequest {
ProfileUpdateRequest that = (ProfileUpdateRequest) o;
return Objects.equals(username, that.username) &&
Objects.equals(email, that.email) &&
Objects.equals(fullName, that.fullName) &&
Objects.equals(firstName, that.firstName) &&
Objects.equals(lastName, that.lastName) &&
Objects.equals(phone, that.phone) &&
Objects.equals(password, that.password);
}
@Override
public int hashCode() {
return Objects.hash(username, email, fullName, phone, password);
return Objects.hash(username, email, firstName, lastName, phone, password);
}
@Override
@@ -82,7 +94,8 @@ public class ProfileUpdateRequest {
return "ProfileUpdateRequest{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", phone='" + phone + '\'' +
", password='" + password + '\'' +
'}';

View File

@@ -18,9 +18,13 @@ public class RegisterRequest {
@Email(message = "Email must be valid")
private String email;
@NotBlank(message = "Full name is required")
@Size(max = 100, message = "Full name must not exceed 100 characters")
private String fullName;
@NotBlank(message = "First name is required")
@Size(max = 50, message = "First name must not exceed 50 characters")
private String firstName;
@NotBlank(message = "Last name is required")
@Size(max = 50, message = "Last name must not exceed 50 characters")
private String lastName;
@NotBlank(message = "Phone is required")
@Size(max = 20, message = "Phone must not exceed 20 characters")
@@ -50,12 +54,20 @@ public class RegisterRequest {
this.email = email;
}
public String getFullName() {
return fullName;
public String getFirstName() {
return firstName;
}
public void setFullName(String fullName) {
this.fullName = fullName;
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getPhone() {
@@ -74,13 +86,14 @@ public class RegisterRequest {
return Objects.equals(username, that.username) &&
Objects.equals(password, that.password) &&
Objects.equals(email, that.email) &&
Objects.equals(fullName, that.fullName) &&
Objects.equals(firstName, that.firstName) &&
Objects.equals(lastName, that.lastName) &&
Objects.equals(phone, that.phone);
}
@Override
public int hashCode() {
return Objects.hash(username, password, email, fullName, phone);
return Objects.hash(username, password, email, firstName, lastName, phone);
}
@Override
@@ -89,7 +102,8 @@ public class RegisterRequest {
"username='" + username + '\'' +
", password='" + password + '\'' +
", email='" + email + '\'' +
", fullName='" + fullName + '\'' +
", firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", phone='" + phone + '\'' +
'}';
}

View File

@@ -5,6 +5,8 @@ import java.util.Objects;
public class UserInfoResponse {
private Long id;
private String username;
private String firstName;
private String lastName;
private String email;
private String fullName;
private String phone;
@@ -17,9 +19,11 @@ public class UserInfoResponse {
public UserInfoResponse() {
}
public UserInfoResponse(Long id, String username, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) {
public UserInfoResponse(Long id, String username, String firstName, String lastName, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) {
this.id = id;
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.fullName = fullName;
this.phone = phone;
@@ -46,6 +50,22 @@ public class UserInfoResponse {
this.username = username;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}

View File

@@ -1,5 +1,7 @@
package com.petshop.backend.dto.pet;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@@ -16,6 +18,10 @@ public class MyPetRequest {
@Size(max = 50, message = "Breed must not exceed 50 characters")
private String breed;
@Min(value = 0, message = "Age must be 0 or greater")
@Max(value = 100, message = "Age must not exceed 100")
private Integer petAge;
public String getPetName() {
return petName;
}
@@ -39,4 +45,12 @@ public class MyPetRequest {
public void setBreed(String breed) {
this.breed = breed;
}
public Integer getPetAge() {
return petAge;
}
public void setPetAge(Integer petAge) {
this.petAge = petAge;
}
}

View File

@@ -6,17 +6,21 @@ public class MyPetResponse {
private String petName;
private String species;
private String breed;
private Integer petAge;
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, Integer petAge, String imageUrl, String petStatus) {
this.customerPetId = customerPetId;
this.petName = petName;
this.species = species;
this.breed = breed;
this.petAge = petAge;
this.imageUrl = imageUrl;
this.petStatus = petStatus;
}
public Long getCustomerPetId() {
@@ -51,6 +55,14 @@ public class MyPetResponse {
this.breed = breed;
}
public Integer getPetAge() {
return petAge;
}
public void setPetAge(Integer petAge) {
this.petAge = petAge;
}
public String getImageUrl() {
return imageUrl;
}
@@ -58,4 +70,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

@@ -51,5 +51,7 @@ 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);
List<Appointment> findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status);
}

View File

@@ -66,6 +66,7 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/services/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/stores/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/pet-species").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/pet-breeds").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/stores").permitAll()
@@ -101,7 +102,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

@@ -158,6 +158,57 @@ public class AdoptionService {
return mapToResponse(adoption);
}
@Transactional
public AdoptionResponse requestAdoption(Long customerId, Long petId, Long employeeId, Long sourceStoreId, LocalDate adoptionDate) {
Pet pet = petRepository.findById(petId)
.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(adoptionDate);
adoption.setAdoptionStatus(ADOPTION_STATUS_PENDING);
adoption = adoptionRepository.save(adoption);
syncPetStatus(pet, ADOPTION_STATUS_PENDING, adoption.getAdoptionId(), customer);
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) {
Adoption adoption = adoptionRepository.findById(id)

View File

@@ -109,6 +109,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);
validatePetServiceCompatibility(pet, service);
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
@@ -167,6 +183,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)) {
@@ -350,6 +383,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,9 @@ public class PetService {
pet.getPetName(),
pet.getPetSpecies(),
pet.getPetBreed(),
pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null
pet.getPetAge(),
pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null,
pet.getPetStatus()
);
}
@@ -349,7 +367,7 @@ public class PetService {
pet.setPetName(request.getPetName().trim());
pet.setPetSpecies(request.getSpecies().trim());
pet.setPetBreed(normalizeOptional(request.getBreed()));
pet.setPetAge(null);
pet.setPetAge(request.getPetAge());
pet.setPetPrice(null);
}