From 46bdd5c3d725d5628ebce845c92e1bd6cae0822f Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 13:02:00 -0600 Subject: [PATCH 01/14] fix tests and silent failures --- .../backend/controller/AuthController.java | 1 + .../backend/service/AppointmentService.java | 24 ++++++++++++------- .../petshop/backend/service/CartService.java | 2 +- .../backend/service/ProductService.java | 7 +++++- .../petshop/backend/service/SaleService.java | 7 ++++-- .../backend/service/UserServiceTest.java | 9 +++---- 6 files changed, 34 insertions(+), 16 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index 1b4ed0bf..58ac9d06 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -410,6 +410,7 @@ public class AuthController { try { avatarStorageService.deleteAvatar(user); } catch (IOException e) { + log.warn("Failed to delete avatar for user {}: {}", user.getId(), e.getMessage()); } user.setAvatarUrl(null); userRepository.save(user); diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index ad29a08c..bc8ece99 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -326,17 +326,25 @@ public class AppointmentService { AppointmentResponse response = new AppointmentResponse(); response.setAppointmentId(appointment.getAppointmentId()); - response.setCustomerId(appointment.getCustomer().getId()); - response.setCustomerName(appointment.getCustomer().getFirstName() + " " + appointment.getCustomer().getLastName()); - response.setStoreId(appointment.getStore().getStoreId()); - response.setStoreName(appointment.getStore().getStoreName()); - response.setServiceId(appointment.getService().getServiceId()); - response.setServiceName(appointment.getService().getServiceName()); + if (appointment.getCustomer() != null) { + response.setCustomerId(appointment.getCustomer().getId()); + response.setCustomerName(appointment.getCustomer().getFirstName() + " " + appointment.getCustomer().getLastName()); + } + if (appointment.getStore() != null) { + response.setStoreId(appointment.getStore().getStoreId()); + response.setStoreName(appointment.getStore().getStoreName()); + } + if (appointment.getService() != null) { + response.setServiceId(appointment.getService().getServiceId()); + response.setServiceName(appointment.getService().getServiceName()); + } response.setAppointmentDate(appointment.getAppointmentDate()); response.setAppointmentTime(appointment.getAppointmentTime()); response.setAppointmentStatus(appointment.getAppointmentStatus()); - response.setEmployeeId(appointment.getEmployee().getId()); - response.setEmployeeName(appointment.getEmployee().getFirstName() + " " + appointment.getEmployee().getLastName()); + if (appointment.getEmployee() != null) { + response.setEmployeeId(appointment.getEmployee().getId()); + response.setEmployeeName(appointment.getEmployee().getFirstName() + " " + appointment.getEmployee().getLastName()); + } response.setPetName(pet != null ? pet.getPetName() : null); response.setPetId(pet != null ? pet.getPetId() : null); response.setCreatedAt(appointment.getCreatedAt()); diff --git a/backend/src/main/java/com/petshop/backend/service/CartService.java b/backend/src/main/java/com/petshop/backend/service/CartService.java index 54844dc3..200698f0 100644 --- a/backend/src/main/java/com/petshop/backend/service/CartService.java +++ b/backend/src/main/java/com/petshop/backend/service/CartService.java @@ -12,7 +12,7 @@ import com.stripe.exception.StripeException; import com.stripe.model.PaymentIntent; import com.stripe.param.PaymentIntentCreateParams; import jakarta.annotation.PostConstruct; -import jakarta.transaction.Transactional; +import org.springframework.transaction.annotation.Transactional; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; diff --git a/backend/src/main/java/com/petshop/backend/service/ProductService.java b/backend/src/main/java/com/petshop/backend/service/ProductService.java index 38f30e84..0b71d5b2 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductService.java @@ -9,6 +9,8 @@ import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.CategoryRepository; import com.petshop.backend.repository.ProductRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.data.domain.Page; @@ -23,6 +25,8 @@ import java.util.Locale; @Service public class ProductService { + private static final Logger log = LoggerFactory.getLogger(ProductService.class); + private final ProductRepository productRepository; private final CategoryRepository categoryRepository; private final CatalogImageStorageService catalogImageStorageService; @@ -152,7 +156,8 @@ public class ProductService { } try { catalogImageStorageService.deleteProductImage(storedImagePath); - } catch (IOException | IllegalArgumentException ignored) { + } catch (IOException | IllegalArgumentException e) { + log.warn("Failed to delete stored image {}: {}", storedImagePath, e.getMessage()); } } diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index 20dc324d..d43919bc 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -367,8 +367,11 @@ public class SaleService { SaleResponse response = new SaleResponse(); response.setSaleId(sale.getSaleId()); response.setSaleDate(sale.getSaleDate()); - response.setEmployeeId(sale.getEmployee().getId()); - response.setEmployeeName(sale.getEmployee().getFirstName() + " " + sale.getEmployee().getLastName()); + + if (sale.getEmployee() != null) { + response.setEmployeeId(sale.getEmployee().getId()); + response.setEmployeeName(sale.getEmployee().getFirstName() + " " + sale.getEmployee().getLastName()); + } if (sale.getStore() != null) { response.setStoreId(sale.getStore().getStoreId()); diff --git a/backend/src/test/java/com/petshop/backend/service/UserServiceTest.java b/backend/src/test/java/com/petshop/backend/service/UserServiceTest.java index 165c869b..66782200 100644 --- a/backend/src/test/java/com/petshop/backend/service/UserServiceTest.java +++ b/backend/src/test/java/com/petshop/backend/service/UserServiceTest.java @@ -59,7 +59,7 @@ class UserServiceTest { User target = user(2L, User.Role.ADMIN); authenticate(actor); - when(userRepository.findById(2L)).thenReturn(Optional.of(target)); + when(userRepository.findByIdForUpdate(2L)).thenReturn(Optional.of(target)); when(userRepository.findById(1L)).thenReturn(Optional.of(actor)); UserRequest request = request(User.Role.ADMIN); @@ -74,7 +74,7 @@ class UserServiceTest { User target = user(2L, User.Role.ADMIN); authenticate(actor); - when(userRepository.findById(2L)).thenReturn(Optional.of(target)); + when(userRepository.findByIdForUpdate(2L)).thenReturn(Optional.of(target)); UserRequest request = request(User.Role.CUSTOMER); @@ -145,6 +145,7 @@ class UserServiceTest { User actor = user(1L, User.Role.ADMIN); authenticate(actor); + when(userRepository.findByIdForUpdate(1L)).thenReturn(Optional.of(actor)); when(userRepository.findById(1L)).thenReturn(Optional.of(actor)); when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0)); @@ -161,7 +162,7 @@ class UserServiceTest { User target = user(2L, User.Role.CUSTOMER); authenticate(actor); - when(userRepository.findById(2L)).thenReturn(Optional.of(target)); + when(userRepository.findByIdForUpdate(2L)).thenReturn(Optional.of(target)); when(userRepository.findById(1L)).thenReturn(Optional.of(actor)); UserRequest request = request(User.Role.ADMIN); @@ -176,7 +177,7 @@ class UserServiceTest { User target = user(2L, User.Role.CUSTOMER); authenticate(actor); - when(userRepository.findById(2L)).thenReturn(Optional.of(target)); + when(userRepository.findByIdForUpdate(2L)).thenReturn(Optional.of(target)); UserRequest request = request(User.Role.ADMIN); From 4162e34a5feb143f2b62db336001e107032a6c92 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 13:31:00 -0600 Subject: [PATCH 02/14] unify error handling --- .../backend/controller/AuthController.java | 89 +++++-------------- .../backend/exception/ConflictException.java | 7 ++ .../exception/GlobalExceptionHandler.java | 17 ++++ .../backend/service/AdoptionService.java | 19 ++-- .../backend/service/AppointmentService.java | 16 ++-- .../backend/service/CouponService.java | 5 +- .../petshop/backend/service/PetService.java | 11 +-- .../backend/service/ProductService.java | 8 +- .../backend/util/AuthenticationHelper.java | 11 ++- 9 files changed, 85 insertions(+), 98 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/exception/ConflictException.java diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index 58ac9d06..1b88551c 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -13,6 +13,9 @@ import com.petshop.backend.dto.auth.ResetPasswordResponse; import com.petshop.backend.dto.auth.UserInfoResponse; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; +import com.petshop.backend.exception.BusinessException; +import com.petshop.backend.exception.ConflictException; +import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.security.JwtUtil; import com.petshop.backend.security.UserAuthCacheService; @@ -74,7 +77,7 @@ public class AuthController { } @PostMapping("/register") - public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { String username = trimToNull(request.getUsername()); String email = trimToNull(request.getEmail()); String firstName = trimToNull(request.getFirstName()); @@ -83,23 +86,17 @@ public class AuthController { if (userRepository.findByUsername(username).isPresent()) { log.warn("Registration rejected: username already exists ({})", username); - Map error = new HashMap<>(); - error.put("message", "Username already exists"); - return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + throw new ConflictException("Username already exists"); } if (userRepository.findByEmail(email).isPresent()) { log.warn("Registration rejected: email already exists"); - Map error = new HashMap<>(); - error.put("message", "Email already exists"); - return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + throw new ConflictException("Email already exists"); } if (phone != null && userRepository.findByPhone(phone).isPresent()) { log.warn("Registration rejected: phone already exists"); - Map error = new HashMap<>(); - error.put("message", "Phone already exists"); - return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + throw new ConflictException("Phone already exists"); } User user = new User(); @@ -117,9 +114,7 @@ public class AuthController { try { savedUser = userRepository.save(user); } catch (DataIntegrityViolationException e) { - Map error = new HashMap<>(); - error.put("message", "Username, email, or phone already exists"); - return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + throw new ConflictException("Username, email, or phone already exists"); } emailService.sendWelcome(savedUser); @@ -137,7 +132,7 @@ public class AuthController { } @PostMapping("/login") - public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { try { authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()) @@ -159,21 +154,15 @@ public class AuthController { } catch (BadCredentialsException e) { log.warn("Login failed for username {}", request.getUsername()); - Map error = new HashMap<>(); - error.put("message", "Invalid username or password"); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error); + throw e; } catch (InternalAuthenticationServiceException e) { if (e.getCause() instanceof DisabledException disabledException) { log.warn("Login denied for disabled user {}", request.getUsername()); - Map error = new HashMap<>(); - error.put("message", disabledException.getMessage()); - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error); + throw disabledException; } throw e; } catch (DisabledException e) { - Map error = new HashMap<>(); - error.put("message", e.getMessage()); - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error); + throw e; } } @@ -196,7 +185,7 @@ public class AuthController { @Transactional @PutMapping("/me") - public ResponseEntity updateProfile(@Valid @RequestBody ProfileUpdateRequest request) { + public ResponseEntity updateProfile(@Valid @RequestBody ProfileUpdateRequest request) { Long userId = AuthenticationHelper.getAuthenticatedUserId(); User user = userRepository.findByIdForUpdate(userId) .orElseThrow(() -> new UsernameNotFoundException("User not found")); @@ -205,9 +194,7 @@ public class AuthController { String username = trimToNull(request.getUsername()); if (username != null && !username.equals(user.getUsername())) { if (userRepository.findByUsername(username).isPresent()) { - Map error = new HashMap<>(); - error.put("message", "Username already exists"); - return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + throw new ConflictException("Username already exists"); } user.setUsername(username); invalidateToken = true; @@ -216,9 +203,7 @@ public class AuthController { String email = trimToNull(request.getEmail()); if (email != null && !email.equals(user.getEmail())) { if (userRepository.findByEmail(email).isPresent()) { - Map error = new HashMap<>(); - error.put("message", "Email already exists"); - return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + throw new ConflictException("Email already exists"); } user.setEmail(email); } @@ -241,9 +226,7 @@ public class AuthController { if (phone != null && userRepository.findByPhone(phone) .filter(existing -> !existing.getId().equals(user.getId())) .isPresent()) { - Map error = new HashMap<>(); - error.put("message", "Phone already exists"); - return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + throw new ConflictException("Phone already exists"); } user.setPhone(phone); } @@ -262,9 +245,7 @@ public class AuthController { try { updatedUser = userRepository.save(user); } catch (DataIntegrityViolationException e) { - Map error = new HashMap<>(); - error.put("message", "Username, email, or phone already exists"); - return ResponseEntity.status(HttpStatus.CONFLICT).body(error); + throw new ConflictException("Username, email, or phone already exists"); } userAuthCacheService.evict(updatedUser.getId()); return ResponseEntity.ok(toUserInfoResponse(updatedUser)); @@ -306,17 +287,6 @@ public class AuthController { return trimToNull(PhoneUtils.normalize(trimToNull(value))); } - private NameParts splitFullName(String value) { - String normalized = trimToNull(value); - if (normalized == null) { - throw new IllegalArgumentException("Full name is required"); - } - String[] parts = normalized.split("\\s+", 2); - String firstName = parts[0]; - String lastName = parts.length > 1 ? parts[1] : ""; - return new NameParts(firstName, lastName, joinFullName(firstName, lastName)); - } - private String joinFullName(String firstName, String lastName) { String first = trimToNull(firstName); String last = trimToNull(lastName); @@ -329,30 +299,21 @@ public class AuthController { return first + " " + last; } - private record NameParts(String firstName, String lastName, String fullName) { - } - @PostMapping("/me/avatar") - public ResponseEntity uploadAvatar(@RequestParam("avatar") MultipartFile file) { + public ResponseEntity uploadAvatar(@RequestParam("avatar") MultipartFile file) { User user = getAuthenticatedUser(); if (file.isEmpty()) { - Map error = new HashMap<>(); - error.put("message", "Please select a file to upload"); - return ResponseEntity.badRequest().body(error); + throw new BusinessException("Please select a file to upload"); } if (file.getSize() > 5 * 1024 * 1024) { - Map error = new HashMap<>(); - error.put("message", "File size must not exceed 5MB"); - return ResponseEntity.badRequest().body(error); + throw new BusinessException("File size must not exceed 5MB"); } String contentType = file.getContentType(); if (contentType == null || (!contentType.equals("image/jpeg") && !contentType.equals("image/png") && !contentType.equals("image/gif"))) { - Map error = new HashMap<>(); - error.put("message", "Only JPG, PNG, and GIF images are allowed"); - return ResponseEntity.badRequest().body(error); + throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); } try { @@ -364,9 +325,7 @@ public class AuthController { return ResponseEntity.ok(new AvatarUploadResponse(avatarStorageService.toOwnerAvatarUrl(user), "Avatar uploaded successfully")); } catch (IOException e) { - Map error = new HashMap<>(); - error.put("message", "Failed to upload avatar: " + e.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + throw new BusinessException("Failed to upload avatar: " + e.getMessage()); } } @@ -375,9 +334,7 @@ public class AuthController { User user = getAuthenticatedUser(); if (!avatarStorageService.hasAvatar(user)) { - Map error = new HashMap<>(); - error.put("message", "No avatar uploaded"); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error); + throw new ResourceNotFoundException("No avatar uploaded"); } Map response = new HashMap<>(); diff --git a/backend/src/main/java/com/petshop/backend/exception/ConflictException.java b/backend/src/main/java/com/petshop/backend/exception/ConflictException.java new file mode 100644 index 00000000..a0112782 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/exception/ConflictException.java @@ -0,0 +1,7 @@ +package com.petshop.backend.exception; + +public class ConflictException extends RuntimeException { + public ConflictException(String message) { + super(message); + } +} diff --git a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java index 7c2b475f..29b7f10d 100644 --- a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java @@ -5,6 +5,8 @@ import jakarta.servlet.http.HttpServletRequest; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.core.PropertyReferenceException; import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.DisabledException; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; @@ -33,6 +35,11 @@ public class GlobalExceptionHandler { return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), ex, request); } + @ExceptionHandler(ConflictException.class) + public ResponseEntity handleConflictException(ConflictException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.CONFLICT, ex.getMessage(), ex, request); + } + @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity> handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) { Map errors = new HashMap<>(); @@ -54,6 +61,16 @@ public class GlobalExceptionHandler { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } + @ExceptionHandler(BadCredentialsException.class) + public ResponseEntity handleBadCredentials(BadCredentialsException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.UNAUTHORIZED, "Invalid username or password", ex, request); + } + + @ExceptionHandler(DisabledException.class) + public ResponseEntity handleDisabledException(DisabledException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.FORBIDDEN, ex.getMessage(), ex, request); + } + @ExceptionHandler(org.springframework.security.access.AccessDeniedException.class) public ResponseEntity handleAccessDeniedException(org.springframework.security.access.AccessDeniedException ex, HttpServletRequest request) { return buildErrorResponse(HttpStatus.FORBIDDEN, ex.getMessage(), ex, request); diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 64eb1c11..8af4a82c 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -8,6 +8,7 @@ import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.Sale; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; +import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.event.AdoptionConfirmedEvent; import com.petshop.backend.event.SaleReceiptEvent; @@ -96,7 +97,7 @@ public class AdoptionService { String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus()); validatePetAvailability(pet, null, null); if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) && request.getAdoptionDate() != null && request.getAdoptionDate().isAfter(LocalDate.now())) { - throw new IllegalArgumentException("Cannot mark a future-dated adoption as Completed"); + throw new BusinessException("Cannot mark a future-dated adoption as Completed"); } Adoption adoption = new Adoption(); @@ -139,7 +140,7 @@ public class AdoptionService { Long currentPetId = adoption.getPet() != null ? adoption.getPet().getPetId() : null; validatePetAvailability(pet, adoption.getAdoptionId(), currentPetId); if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) && request.getAdoptionDate() != null && request.getAdoptionDate().isAfter(LocalDate.now())) { - throw new IllegalArgumentException("Cannot mark a future-dated adoption as Completed"); + throw new BusinessException("Cannot mark a future-dated adoption as Completed"); } adoption.setPet(pet); @@ -168,7 +169,7 @@ public class AdoptionService { // 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."); + throw new BusinessException("The specified pet is not located at the selected store."); } // Verify the pet is available for adoption @@ -203,7 +204,7 @@ public class AdoptionService { } if (!ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoption.getAdoptionStatus())) { - throw new IllegalArgumentException("Only pending adoptions can be cancelled"); + throw new BusinessException("Only pending adoptions can be cancelled"); } adoption.setAdoptionStatus(ADOPTION_STATUS_CANCELLED); @@ -280,7 +281,7 @@ public class AdoptionService { User employee = userRepository.findById(requestedEmployeeId) .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId)); if (!isAssignableUser(employee)) { - throw new IllegalArgumentException("Selected employee is not assignable for adoption work"); + throw new BusinessException("Selected employee is not assignable for adoption work"); } return employee; } @@ -295,7 +296,7 @@ public class AdoptionService { private String normalizeAdoptionStatus(String adoptionStatus) { if (adoptionStatus == null) { - throw new IllegalArgumentException("Adoption status is required"); + throw new BusinessException("Adoption status is required"); } String trimmedStatus = adoptionStatus.trim(); if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(trimmedStatus)) { @@ -310,7 +311,7 @@ public class AdoptionService { if (ADOPTION_STATUS_MISSED.equalsIgnoreCase(trimmedStatus)) { return ADOPTION_STATUS_MISSED; } - throw new IllegalArgumentException("Adoption status must be Pending, Completed, Cancelled, or Missed"); + throw new BusinessException("Adoption status must be Pending, Completed, Cancelled, or Missed"); } private void validatePetAvailability(Pet pet, Long adoptionId, Long currentPetId) { @@ -319,11 +320,11 @@ public class AdoptionService { ? adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED) : adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); if (adoptedElsewhere) { - throw new IllegalArgumentException("Selected pet has already been adopted"); + throw new BusinessException("Selected pet has already been adopted"); } if (!samePetAsCurrentAdoption && !PET_STATUS_AVAILABLE.equalsIgnoreCase(pet.getPetStatus())) { - throw new IllegalArgumentException("Selected pet is not available for adoption"); + throw new BusinessException("Selected pet is not available for adoption"); } } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index bc8ece99..1c0b2f1d 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -116,14 +116,14 @@ public class AppointmentService { // 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"); + throw new BusinessException("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"); + throw new BusinessException("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"); + throw new BusinessException("Only your own pets can be booked for appointments"); } } @@ -198,7 +198,7 @@ public class AppointmentService { String status = appointment.getAppointmentStatus(); if (!"Booked".equalsIgnoreCase(status) && !"Scheduled".equalsIgnoreCase(status)) { - throw new IllegalArgumentException("Only booked or scheduled appointments can be cancelled"); + throw new BusinessException("Only booked or scheduled appointments can be cancelled"); } appointment.setAppointmentStatus("Cancelled"); @@ -311,7 +311,7 @@ public class AppointmentService { if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) { LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime()); if (appointmentDateTime.isBefore(LocalDateTime.now())) { - throw new IllegalArgumentException("Booked appointments must be scheduled in the future"); + throw new BusinessException("Booked appointments must be scheduled in the future"); } } } @@ -363,7 +363,7 @@ public class AppointmentService { boolean assignedToStore = assignableUsers.stream() .anyMatch(u -> u.getId().equals(requestedEmployeeId)); if (!assignedToStore) { - throw new IllegalArgumentException("Selected employee is not assignable for the selected store"); + throw new BusinessException("Selected employee is not assignable for the selected store"); } return employee; } @@ -377,7 +377,7 @@ public class AppointmentService { List existingAppointments = appointmentRepository .findByEmployeeIdAndAppointmentDate(employee.getId(), date); if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { - throw new IllegalArgumentException("The selected employee is already booked for this time slot"); + throw new BusinessException("The selected employee is already booked for this time slot"); } } @@ -401,7 +401,7 @@ public class AppointmentService { List existingAppointments = appointmentRepository .findByPetIdAndAppointmentDate(pet.getPetId(), date); if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { - throw new IllegalArgumentException("This pet already has an appointment during this time slot"); + throw new BusinessException("This pet already has an appointment during this time slot"); } } diff --git a/backend/src/main/java/com/petshop/backend/service/CouponService.java b/backend/src/main/java/com/petshop/backend/service/CouponService.java index 92d67819..89cac69a 100644 --- a/backend/src/main/java/com/petshop/backend/service/CouponService.java +++ b/backend/src/main/java/com/petshop/backend/service/CouponService.java @@ -4,6 +4,7 @@ import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.dto.common.CouponRequest; import com.petshop.backend.dto.common.CouponResponse; import com.petshop.backend.entity.Coupon; +import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.CouponRepository; import org.springframework.data.domain.Page; @@ -39,7 +40,7 @@ public class CouponService { @Transactional public CouponResponse createCoupon(CouponRequest request) { if (couponRepository.findByCouponCode(request.getCouponCode()).isPresent()) { - throw new IllegalArgumentException("Coupon code already exists: " + request.getCouponCode()); + throw new BusinessException("Coupon code already exists: " + request.getCouponCode()); } Coupon coupon = new Coupon(); @@ -55,7 +56,7 @@ public class CouponService { couponRepository.findByCouponCode(request.getCouponCode()).ifPresent(existing -> { if (!existing.getCouponId().equals(id)) { - throw new IllegalArgumentException("Coupon code already exists: " + request.getCouponCode()); + throw new BusinessException("Coupon code already exists: " + request.getCouponCode()); } }); 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 dd86eb4a..f901b797 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -9,6 +9,7 @@ import com.petshop.backend.entity.Adoption; import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; +import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.security.AppPrincipal; import com.petshop.backend.repository.AdoptionRepository; @@ -125,7 +126,7 @@ public class PetService { boolean hasBooked = linkedAppointments.stream() .anyMatch(a -> "Booked".equalsIgnoreCase(a.getAppointmentStatus())); if (hasBooked) { - throw new IllegalArgumentException( + throw new BusinessException( "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 @@ -280,18 +281,18 @@ public class PetService { private void validateImageFile(MultipartFile file) { if (file == null || file.isEmpty()) { - throw new IllegalArgumentException("Please select an image to upload"); + throw new BusinessException("Please select an image to upload"); } if (file.getSize() > 5 * 1024 * 1024) { - throw new IllegalArgumentException("Image file size must be less than 5MB"); + throw new BusinessException("Image file size must be less than 5MB"); } String contentType = file.getContentType(); if (contentType == null) { - throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed"); + throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); } String normalized = contentType.toLowerCase(Locale.ROOT); if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) { - throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed"); + throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); } } diff --git a/backend/src/main/java/com/petshop/backend/service/ProductService.java b/backend/src/main/java/com/petshop/backend/service/ProductService.java index 0b71d5b2..5234ef22 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductService.java @@ -135,18 +135,18 @@ public class ProductService { private void validateImageFile(MultipartFile file) { if (file == null || file.isEmpty()) { - throw new IllegalArgumentException("Please select an image to upload"); + throw new BusinessException("Please select an image to upload"); } if (file.getSize() > 5 * 1024 * 1024) { - throw new IllegalArgumentException("Image file size must be less than 5MB"); + throw new BusinessException("Image file size must be less than 5MB"); } String contentType = file.getContentType(); if (contentType == null) { - throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed"); + throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); } String normalized = contentType.toLowerCase(Locale.ROOT); if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) { - throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed"); + throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); } } diff --git a/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java index 66c7418f..c78e87d3 100644 --- a/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java +++ b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java @@ -3,8 +3,11 @@ package com.petshop.backend.util; import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.security.AppPrincipal; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; @Component @@ -13,7 +16,7 @@ public class AuthenticationHelper { public static Authentication getAuthentication() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) { - throw new RuntimeException("No authenticated user found"); + throw new AuthenticationCredentialsNotFoundException("No authenticated user found"); } return authentication; } @@ -23,7 +26,7 @@ public class AuthenticationHelper { if (principal instanceof AppPrincipal appPrincipal) { return appPrincipal; } - throw new RuntimeException("Authenticated principal is not supported"); + throw new AuthenticationServiceException("Authenticated principal is not supported"); } public static Long getAuthenticatedUserId() { @@ -36,11 +39,11 @@ public class AuthenticationHelper { if (principal instanceof AppPrincipal appPrincipal) { return userRepository.findById(appPrincipal.getUserId()) - .orElseThrow(() -> new RuntimeException("User not found: " + appPrincipal.getUserId())); + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + appPrincipal.getUserId())); } String username = authentication.getName(); return userRepository.findByUsername(username) - .orElseThrow(() -> new RuntimeException("User not found: " + username)); + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); } } From f7c1ff453f47641514dd8ec7fc14fcdf9a2f500f Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 13:54:00 -0600 Subject: [PATCH 03/14] use shared StringUtils.trimToNull --- .../controller/AdoptionController.java | 41 +------- .../controller/AppointmentController.java | 50 ++-------- .../backend/controller/AuthController.java | 31 +++---- .../backend/controller/RefundController.java | 42 +-------- .../backend/service/AdoptionService.java | 13 +-- .../backend/service/AppointmentService.java | 13 +-- .../petshop/backend/service/CartService.java | 93 +++++-------------- .../backend/service/CategoryService.java | 10 +- .../backend/service/CouponService.java | 56 ++++++++++- .../backend/service/InventoryService.java | 11 +-- .../backend/service/PasswordResetService.java | 12 +-- .../petshop/backend/service/PetService.java | 17 +--- .../backend/service/ProductService.java | 10 +- .../service/ProductSupplierService.java | 11 +-- .../backend/service/PurchaseOrderService.java | 11 +-- .../petshop/backend/service/SaleService.java | 58 ++---------- .../petshop/backend/service/UserService.java | 23 ++--- .../backend/util/AuthenticationHelper.java | 15 +++ .../backend/util/ImageValidationUtil.java | 30 ++++++ .../com/petshop/backend/util/StringUtils.java | 14 +++ 20 files changed, 200 insertions(+), 361 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/util/ImageValidationUtil.java create mode 100644 backend/src/main/java/com/petshop/backend/util/StringUtils.java diff --git a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java index ee577f34..84005778 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -14,8 +14,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; @@ -41,17 +39,10 @@ public class AdoptionController { @RequestParam(required = false) Long storeId, @RequestParam(required = false) String date, Pageable pageable) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - Long effectiveCustomerId = customerId; - if (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - effectiveCustomerId = user.getId(); - } + Long effectiveCustomerId = AuthenticationHelper.isCustomer() + ? AuthenticationHelper.getAuthenticatedUser(userRepository).getId() + : customerId; LocalDate adoptionDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null; @@ -61,18 +52,7 @@ public class AdoptionController { @GetMapping("/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAdoptionById(@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 (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.ok(adoptionService.getAdoptionById(id, customerId)); } @@ -94,18 +74,7 @@ public class AdoptionController { @PatchMapping("/{id}/cancel") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity 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(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.ok(adoptionService.cancelAdoption(id, customerId)); } diff --git a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java index e0214fa7..364f836e 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -13,8 +13,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; @@ -43,17 +41,9 @@ public class AppointmentController { @RequestParam(required = false) Long employeeId, Pageable pageable) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - Long effectiveCustomerId = customerId; - if ("CUSTOMER".equals(role)) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - effectiveCustomerId = user.getId(); - } + Long effectiveCustomerId = AuthenticationHelper.isCustomer() + ? AuthenticationHelper.getAuthenticatedUser(userRepository).getId() + : customerId; LocalDate appointmentDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null; @@ -64,31 +54,14 @@ public class AppointmentController { @GetMapping("/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAppointmentById(@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 (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.ok(appointmentService.getAppointmentById(id, customerId)); } @PostMapping @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity createAppointment(@Valid @RequestBody AppointmentRequest request) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - if ("CUSTOMER".equals(role)) { + if (AuthenticationHelper.isCustomer()) { User user = AuthenticationHelper.getAuthenticatedUser(userRepository); if (!request.getCustomerId().equals(user.getId())) { throw new org.springframework.security.access.AccessDeniedException("You can only create appointments for yourself"); @@ -101,18 +74,7 @@ public class AppointmentController { @PatchMapping("/{id}/cancel") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity 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(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.ok(appointmentService.cancelAppointment(id, customerId)); } diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index 1b88551c..ad1e9be4 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -25,6 +25,7 @@ import com.petshop.backend.service.EmailService; import com.petshop.backend.service.PasswordResetService; import com.petshop.backend.util.AuthenticationHelper; import com.petshop.backend.util.PhoneUtils; +import com.petshop.backend.util.StringUtils; import jakarta.validation.Valid; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; @@ -78,10 +79,10 @@ public class AuthController { @PostMapping("/register") public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { - String username = trimToNull(request.getUsername()); - String email = trimToNull(request.getEmail()); - String firstName = trimToNull(request.getFirstName()); - String lastName = trimToNull(request.getLastName()); + String username = StringUtils.trimToNull(request.getUsername()); + String email = StringUtils.trimToNull(request.getEmail()); + String firstName = StringUtils.trimToNull(request.getFirstName()); + String lastName = StringUtils.trimToNull(request.getLastName()); String phone = normalizePhone(request.getPhone()); if (userRepository.findByUsername(username).isPresent()) { @@ -191,7 +192,7 @@ public class AuthController { .orElseThrow(() -> new UsernameNotFoundException("User not found")); boolean invalidateToken = false; - String username = trimToNull(request.getUsername()); + String username = StringUtils.trimToNull(request.getUsername()); if (username != null && !username.equals(user.getUsername())) { if (userRepository.findByUsername(username).isPresent()) { throw new ConflictException("Username already exists"); @@ -200,7 +201,7 @@ public class AuthController { invalidateToken = true; } - String email = trimToNull(request.getEmail()); + String email = StringUtils.trimToNull(request.getEmail()); if (email != null && !email.equals(user.getEmail())) { if (userRepository.findByEmail(email).isPresent()) { throw new ConflictException("Email already exists"); @@ -208,11 +209,11 @@ public class AuthController { user.setEmail(email); } - String firstName = trimToNull(request.getFirstName()); + String firstName = StringUtils.trimToNull(request.getFirstName()); if (firstName != null) { user.setFirstName(firstName); } - String lastName = trimToNull(request.getLastName()); + String lastName = StringUtils.trimToNull(request.getLastName()); if (lastName != null) { user.setLastName(lastName); } @@ -275,21 +276,13 @@ public class AuthController { ); } - private String trimToNull(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private String normalizePhone(String value) { - return trimToNull(PhoneUtils.normalize(trimToNull(value))); + return StringUtils.trimToNull(PhoneUtils.normalize(StringUtils.trimToNull(value))); } private String joinFullName(String firstName, String lastName) { - String first = trimToNull(firstName); - String last = trimToNull(lastName); + String first = StringUtils.trimToNull(firstName); + String last = StringUtils.trimToNull(lastName); if (first == null) { return last == null ? null : last; } diff --git a/backend/src/main/java/com/petshop/backend/controller/RefundController.java b/backend/src/main/java/com/petshop/backend/controller/RefundController.java index 92fd22b3..da58c6a0 100644 --- a/backend/src/main/java/com/petshop/backend/controller/RefundController.java +++ b/backend/src/main/java/com/petshop/backend/controller/RefundController.java @@ -3,7 +3,6 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.refund.RefundRequest; import com.petshop.backend.dto.refund.RefundResponse; import com.petshop.backend.dto.refund.RefundUpdateRequest; -import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.RefundService; import com.petshop.backend.util.AuthenticationHelper; @@ -11,8 +10,6 @@ import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -32,36 +29,14 @@ public class RefundController { @PostMapping @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF')") public ResponseEntity createRefund(@Valid @RequestBody RefundRequest request) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - Long customerId = null; - if (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.status(HttpStatus.CREATED).body(refundService.createRefund(request, customerId)); } @GetMapping @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity> getAllRefunds() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - Long customerId = null; - if (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); List refunds = refundService.getAllRefunds(customerId); return ResponseEntity.ok(refunds); } @@ -69,18 +44,7 @@ public class RefundController { @GetMapping("/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getRefundById(@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 (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.ok(refundService.getRefundById(id, customerId)); } diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 8af4a82c..679aa18c 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.adoption.AdoptionRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.adoption.AdoptionResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.Adoption; @@ -56,8 +57,8 @@ public class AdoptionService { } public Page getAllAdoptions(String query, Long customerId, String status, Long storeId, LocalDate date, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); - String normalizedStatus = normalizeFilter(status); + String normalizedQuery = StringUtils.trimToNull(query); + String normalizedStatus = StringUtils.trimToNull(status); Page adoptions = adoptionRepository.searchAdoptions( normalizedQuery, @@ -248,14 +249,6 @@ public class AdoptionService { } } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private AdoptionResponse mapToResponse(Adoption adoption) { StoreLocation sourceStore = adoption.getSourceStore(); return new AdoptionResponse( diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 1c0b2f1d..59f290a8 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -20,6 +20,7 @@ import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.util.AuthenticationHelper; +import com.petshop.backend.util.StringUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -67,8 +68,8 @@ public class AppointmentService { LocalDate date, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); - String normalizedStatus = normalizeFilter(status); + String normalizedQuery = StringUtils.trimToNull(query); + String normalizedStatus = StringUtils.trimToNull(status); Page appointments = appointmentRepository.searchAppointments( normalizedQuery, @@ -288,14 +289,6 @@ public class AppointmentService { } } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private void validatePetServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) { if (pet == null) return; Set allowed = service.getSpecies(); diff --git a/backend/src/main/java/com/petshop/backend/service/CartService.java b/backend/src/main/java/com/petshop/backend/service/CartService.java index 200698f0..9e11ac13 100644 --- a/backend/src/main/java/com/petshop/backend/service/CartService.java +++ b/backend/src/main/java/com/petshop/backend/service/CartService.java @@ -32,6 +32,7 @@ public class CartService { private final StoreRepository storeRepository; private final ProductRepository productRepository; private final CouponRepository couponRepository; + private final CouponService couponService; private final SaleRepository saleRepository; private final SaleService saleService; @@ -44,6 +45,7 @@ public class CartService { StoreRepository storeRepository, ProductRepository productRepository, CouponRepository couponRepository, + CouponService couponService, SaleRepository saleRepository, SaleService saleService) { this.cartRepository = cartRepository; @@ -52,6 +54,7 @@ public class CartService { this.storeRepository = storeRepository; this.productRepository = productRepository; this.couponRepository = couponRepository; + this.couponService = couponService; this.saleRepository = saleRepository; this.saleService = saleService; } @@ -188,31 +191,12 @@ public class CartService { Coupon coupon = couponRepository.findByCouponCodeIgnoreCase(couponCode) .orElseThrow(() -> new BusinessException("Invalid coupon code")); - LocalDateTime now = LocalDateTime.now(); - - if (!coupon.getActive()) { - throw new BusinessException("Coupon is no longer active"); - } - - if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) { - throw new BusinessException("Coupon is not yet valid"); - } - - if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) { - throw new BusinessException("Coupon has expired"); - } + couponService.validateCouponForUse(coupon); if (coupon.getMinOrderAmount() != null && cart.getSubtotalAmount().compareTo(coupon.getMinOrderAmount()) < 0) { throw new BusinessException("Minimum order amount of $" + coupon.getMinOrderAmount() + " required"); } - if (coupon.getUsageLimit() != null) { - long used = saleRepository.countByCoupon_CouponId(coupon.getCouponId()); - if (used >= coupon.getUsageLimit()) { - throw new BusinessException("Coupon usage limit has been reached"); - } - } - cart.setCoupon(coupon); recalculate(cart); @@ -450,73 +434,38 @@ public class CartService { } } - private void recalculate(Cart cart) { + private record CartTotals(BigDecimal subtotal, BigDecimal discount, BigDecimal pointsDiscount, BigDecimal total) {} + + private CartTotals computeTotals(Cart cart) { List items = cartItemRepository.findByCartCartId(cart.getCartId()); BigDecimal subtotal = items.stream() .map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(i.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); - cart.setSubtotalAmount(subtotal); - - BigDecimal discount = BigDecimal.ZERO; - Coupon coupon = cart.getCoupon(); - - if (coupon != null) { - if (isPercentageType(coupon.getDiscountType())) { - discount = subtotal.multiply(coupon.getDiscountValue()) - .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); - } else if (isFixedType(coupon.getDiscountType())) { - discount = coupon.getDiscountValue().min(subtotal); - } - } - - discount = discount.max(BigDecimal.ZERO).min(subtotal); - cart.setDiscountAmount(discount); + BigDecimal discount = couponService.calculateDiscount(cart.getCoupon(), subtotal) + .max(BigDecimal.ZERO).min(subtotal); BigDecimal remainingAfterCoupon = subtotal.subtract(discount).max(BigDecimal.ZERO); BigDecimal pointsDiscount = calculatePointsDiscount(cart.getUser(), remainingAfterCoupon, Boolean.TRUE.equals(cart.getPointsApplied())); - cart.setPointsDiscountAmount(pointsDiscount); - cart.setTotalAmount(remainingAfterCoupon.subtract(pointsDiscount).max(BigDecimal.ZERO)); + BigDecimal total = remainingAfterCoupon.subtract(pointsDiscount).max(BigDecimal.ZERO); + + return new CartTotals(subtotal, discount, pointsDiscount, total); + } + + private void recalculate(Cart cart) { + CartTotals totals = computeTotals(cart); + cart.setSubtotalAmount(totals.subtotal()); + cart.setDiscountAmount(totals.discount()); + cart.setPointsDiscountAmount(totals.pointsDiscount()); + cart.setTotalAmount(totals.total()); cartRepository.save(cart); } private BigDecimal recalculateTotalAmount(Cart cart) { - List items = cartItemRepository.findByCartCartId(cart.getCartId()); - - BigDecimal subtotal = items.stream() - .map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(i.getQuantity()))) - .reduce(BigDecimal.ZERO, BigDecimal::add); - - BigDecimal discount = BigDecimal.ZERO; - Coupon coupon = cart.getCoupon(); - - if (coupon != null) { - if (isPercentageType(coupon.getDiscountType())) { - discount = subtotal.multiply(coupon.getDiscountValue()) - .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); - } else if (isFixedType(coupon.getDiscountType())) { - discount = coupon.getDiscountValue().min(subtotal); - } - } - - discount = discount.max(BigDecimal.ZERO).min(subtotal); - - BigDecimal remainingAfterCoupon = subtotal.subtract(discount).max(BigDecimal.ZERO); - BigDecimal pointsDiscount = calculatePointsDiscount(cart.getUser(), remainingAfterCoupon, Boolean.TRUE.equals(cart.getPointsApplied())); - - return remainingAfterCoupon.subtract(pointsDiscount).max(BigDecimal.ZERO); + return computeTotals(cart).total(); } - private boolean isPercentageType(String discountType) { - return "PERCENTAGE".equalsIgnoreCase(discountType) || "PERCENT".equalsIgnoreCase(discountType); - } - - private boolean isFixedType(String discountType) { - return "FIXED".equalsIgnoreCase(discountType) || "FLAT".equalsIgnoreCase(discountType); - } - - private BigDecimal calculatePointsDiscount(User user, BigDecimal remainingAmount, boolean pointsApplied) { if (!pointsApplied || user == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) { return BigDecimal.ZERO; diff --git a/backend/src/main/java/com/petshop/backend/service/CategoryService.java b/backend/src/main/java/com/petshop/backend/service/CategoryService.java index 1bf175c7..742ae847 100644 --- a/backend/src/main/java/com/petshop/backend/service/CategoryService.java +++ b/backend/src/main/java/com/petshop/backend/service/CategoryService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.category.CategoryRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.category.CategoryResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.Category; @@ -21,7 +22,7 @@ public class CategoryService { } public Page getAllCategories(String query, String type, Pageable pageable) { - return categoryRepository.searchCategories(normalizeFilter(query), normalizeFilter(type), pageable) + return categoryRepository.searchCategories(StringUtils.trimToNull(query), StringUtils.trimToNull(type), pageable) .map(this::mapToResponse); } @@ -76,11 +77,4 @@ public class CategoryService { ); } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } } diff --git a/backend/src/main/java/com/petshop/backend/service/CouponService.java b/backend/src/main/java/com/petshop/backend/service/CouponService.java index 89cac69a..eb3b605d 100644 --- a/backend/src/main/java/com/petshop/backend/service/CouponService.java +++ b/backend/src/main/java/com/petshop/backend/service/CouponService.java @@ -7,18 +7,25 @@ import com.petshop.backend.entity.Coupon; import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.CouponRepository; +import com.petshop.backend.repository.SaleRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; + @Service public class CouponService { private final CouponRepository couponRepository; + private final SaleRepository saleRepository; - public CouponService(CouponRepository couponRepository) { + public CouponService(CouponRepository couponRepository, SaleRepository saleRepository) { this.couponRepository = couponRepository; + this.saleRepository = saleRepository; } public Page getAllCoupons(String query, Boolean active, Pageable pageable) { @@ -89,6 +96,53 @@ public class CouponService { coupon.setUsageLimit(request.getUsageLimit()); } + public void validateCouponForUse(Coupon coupon) { + if (!Boolean.TRUE.equals(coupon.getActive())) { + throw new BusinessException("Coupon is no longer active"); + } + LocalDateTime now = LocalDateTime.now(); + if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) { + throw new BusinessException("Coupon is not yet valid"); + } + if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) { + throw new BusinessException("Coupon has expired"); + } + if (coupon.getUsageLimit() != null) { + long used = saleRepository.countByCoupon_CouponId(coupon.getCouponId()); + if (used >= coupon.getUsageLimit()) { + throw new BusinessException("Coupon usage limit has been reached"); + } + } + } + + public BigDecimal calculateDiscount(Coupon coupon, BigDecimal subtotal) { + if (coupon == null || subtotal.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) { + return BigDecimal.ZERO; + } + + BigDecimal discount = BigDecimal.ZERO; + String type = coupon.getDiscountType(); + if (isPercentageType(type)) { + discount = subtotal.multiply(coupon.getDiscountValue()) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + } else if (isFixedType(type)) { + discount = coupon.getDiscountValue(); + } + + return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP); + } + + public static boolean isPercentageType(String discountType) { + return "PERCENTAGE".equalsIgnoreCase(discountType) || "PERCENT".equalsIgnoreCase(discountType); + } + + public static boolean isFixedType(String discountType) { + return "FIXED".equalsIgnoreCase(discountType) || "FLAT".equalsIgnoreCase(discountType); + } + private CouponResponse mapToResponse(Coupon coupon) { return new CouponResponse( coupon.getCouponId(), diff --git a/backend/src/main/java/com/petshop/backend/service/InventoryService.java b/backend/src/main/java/com/petshop/backend/service/InventoryService.java index f7e15068..021ed021 100644 --- a/backend/src/main/java/com/petshop/backend/service/InventoryService.java +++ b/backend/src/main/java/com/petshop/backend/service/InventoryService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.inventory.InventoryRequest; import com.petshop.backend.dto.inventory.InventoryResponse; import com.petshop.backend.entity.Inventory; @@ -29,7 +30,7 @@ public class InventoryService { } public Page getAllInventory(String query, Long storeId, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); + String normalizedQuery = StringUtils.trimToNull(query); Page inventory = inventoryRepository.searchInventory(normalizedQuery, storeId, pageable); return inventory.map(this::mapToResponse); } @@ -93,14 +94,6 @@ public class InventoryService { inventoryRepository.deleteAllById(request.getIds()); } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private InventoryResponse mapToResponse(Inventory inventory) { StoreLocation store = inventory.getStore(); return new InventoryResponse( diff --git a/backend/src/main/java/com/petshop/backend/service/PasswordResetService.java b/backend/src/main/java/com/petshop/backend/service/PasswordResetService.java index f90cf6d5..6c5c78f5 100644 --- a/backend/src/main/java/com/petshop/backend/service/PasswordResetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PasswordResetService.java @@ -6,6 +6,7 @@ import com.petshop.backend.entity.PasswordResetToken; import com.petshop.backend.entity.User; import com.petshop.backend.exception.BusinessException; import com.petshop.backend.repository.PasswordResetTokenRepository; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.security.UserAuthCacheService; import org.springframework.security.crypto.password.PasswordEncoder; @@ -47,7 +48,7 @@ public class PasswordResetService { @Transactional public ForgotPasswordResponse createResetToken(String usernameOrEmail) { - String normalized = trimToNull(usernameOrEmail); + String normalized = StringUtils.trimToNull(usernameOrEmail); if (normalized == null) { throw new BusinessException("Username or email is required"); } @@ -83,7 +84,7 @@ public class PasswordResetService { @Transactional public ResetPasswordResponse resetPassword(String rawToken, String newPassword) { - String normalizedToken = trimToNull(rawToken); + String normalizedToken = StringUtils.trimToNull(rawToken); if (normalizedToken == null) { throw new BusinessException("Reset token is required"); } @@ -136,11 +137,4 @@ public class PasswordResetService { } } - private String trimToNull(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } } 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 f901b797..98b63cd8 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.pet.MyPetRequest; import com.petshop.backend.dto.pet.MyPetResponse; import com.petshop.backend.dto.pet.PetRequest; @@ -53,10 +54,10 @@ public class PetService { @Transactional(readOnly = true) public Page getAllPets(String query, String species, String breed, String status, Long storeId, Long customerId, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); - String normalizedSpecies = normalizeFilter(species); - String normalizedBreed = normalizeFilter(breed); - String normalizedStatus = normalizeFilter(status); + String normalizedQuery = StringUtils.trimToNull(query); + String normalizedSpecies = StringUtils.trimToNull(species); + String normalizedBreed = StringUtils.trimToNull(breed); + String normalizedStatus = StringUtils.trimToNull(status); CurrentViewer viewer = getCurrentViewer(); Page pets; @@ -323,14 +324,6 @@ public class PetService { return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status) || "owned".equalsIgnoreCase(status); } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private PetResponse mapToResponse(Pet pet) { User owner = pet.getOwner(); StoreLocation store = pet.getStore(); diff --git a/backend/src/main/java/com/petshop/backend/service/ProductService.java b/backend/src/main/java/com/petshop/backend/service/ProductService.java index 5234ef22..9f2a9069 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.product.ProductRequest; import com.petshop.backend.dto.product.ProductResponse; import com.petshop.backend.entity.Category; @@ -38,7 +39,7 @@ public class ProductService { } public Page getAllProducts(String query, Long categoryId, Pageable pageable) { - return productRepository.searchProducts(normalizeFilter(query), categoryId, pageable) + return productRepository.searchProducts(StringUtils.trimToNull(query), categoryId, pageable) .map(this::mapToResponse); } @@ -178,11 +179,4 @@ public class ProductService { public record ImagePayload(Resource resource, MediaType mediaType) { } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } } diff --git a/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java index c9be85f1..0f6250d2 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.productsupplier.BulkDeleteProductSupplierRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.productsupplier.ProductSupplierRequest; import com.petshop.backend.dto.productsupplier.ProductSupplierResponse; import com.petshop.backend.entity.Product; @@ -34,7 +35,7 @@ public class ProductSupplierService { } public Page getAllProductSuppliers(String query, Long productId, Long supplierId, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); + String normalizedQuery = StringUtils.trimToNull(query); Pageable mappedPageable = mapSortProperties(pageable); Page productSuppliers = productSupplierRepository.searchProductSuppliers(normalizedQuery, productId, supplierId, mappedPageable); return productSuppliers.map(this::mapToResponse); @@ -97,14 +98,6 @@ public class ProductSupplierService { }); } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private Pageable mapSortProperties(Pageable pageable) { if (pageable.getSort().isUnsorted()) { return pageable; diff --git a/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java index f3099354..0f2a1d07 100644 --- a/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java +++ b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.purchaseorder.PurchaseOrderResponse; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.entity.PurchaseOrder; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.exception.ResourceNotFoundException; @@ -19,7 +20,7 @@ public class PurchaseOrderService { } public Page getAllPurchaseOrders(String query, Long storeId, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); + String normalizedQuery = StringUtils.trimToNull(query); Page purchaseOrders = purchaseOrderRepository.searchPurchaseOrders(normalizedQuery, storeId, pageable); return purchaseOrders.map(this::mapToResponse); } @@ -30,14 +31,6 @@ public class PurchaseOrderService { return mapToResponse(purchaseOrder); } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private PurchaseOrderResponse mapToResponse(PurchaseOrder purchaseOrder) { StoreLocation store = purchaseOrder.getStore(); return new PurchaseOrderResponse( diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index d43919bc..f69ef9e1 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -8,6 +8,7 @@ import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.event.SaleReceiptEvent; import com.petshop.backend.repository.*; import com.petshop.backend.util.AuthenticationHelper; +import com.petshop.backend.util.StringUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -32,23 +33,25 @@ public class SaleService { private final InventoryRepository inventoryRepository; private final UserRepository userRepository; private final CouponRepository couponRepository; + private final CouponService couponService; private final CartRepository cartRepository; private final ApplicationEventPublisher eventPublisher; - public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CartRepository cartRepository, ApplicationEventPublisher eventPublisher) { + public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CouponService couponService, CartRepository cartRepository, ApplicationEventPublisher eventPublisher) { this.saleRepository = saleRepository; this.productRepository = productRepository; this.storeRepository = storeRepository; this.inventoryRepository = inventoryRepository; this.userRepository = userRepository; this.couponRepository = couponRepository; + this.couponService = couponService; this.cartRepository = cartRepository; this.eventPublisher = eventPublisher; } @Transactional(readOnly = true) public Page getAllSales(String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId, Pageable pageable) { - Page sales = saleRepository.searchSales(normalizeFilter(query), normalizeFilter(paymentMethod), storeId, isRefund, customerId, pageable); + Page sales = saleRepository.searchSales(StringUtils.trimToNull(query), StringUtils.trimToNull(paymentMethod), storeId, isRefund, customerId, pageable); return sales.map(this::mapToResponse); } @@ -86,7 +89,7 @@ public class SaleService { if (request.getCouponId() != null) { Coupon coupon = couponRepository.findByIdForUpdate(request.getCouponId()) .orElseThrow(() -> new ResourceNotFoundException("Coupon not found with id: " + request.getCouponId())); - validateCoupon(coupon); + couponService.validateCouponForUse(coupon); sale.setCoupon(coupon); } @@ -236,7 +239,7 @@ public class SaleService { } sale.setSubtotalAmount(subtotalAmount); - BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount); + BigDecimal couponDiscount = couponService.calculateDiscount(sale.getCoupon(), subtotalAmount); sale.setCouponDiscountAmount(couponDiscount); BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount)); @@ -282,45 +285,6 @@ public class SaleService { return mapToResponse(savedSale); } - private void validateCoupon(Coupon coupon) { - if (!Boolean.TRUE.equals(coupon.getActive())) { - throw new BusinessException("Coupon is not active"); - } - LocalDateTime now = LocalDateTime.now(); - if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) { - throw new BusinessException("Coupon has not started yet"); - } - if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) { - throw new BusinessException("Coupon has expired"); - } - if (coupon.getUsageLimit() != null) { - long used = saleRepository.countByCoupon_CouponId(coupon.getCouponId()); - if (used >= coupon.getUsageLimit()) { - throw new BusinessException("Coupon usage limit has been reached"); - } - } - } - - private BigDecimal calculateCouponDiscount(Coupon coupon, BigDecimal subtotal) { - if (coupon == null || subtotal.compareTo(BigDecimal.ZERO) <= 0) { - return BigDecimal.ZERO; - } - - if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) { - return BigDecimal.ZERO; - } - - BigDecimal discount = BigDecimal.ZERO; - String type = coupon.getDiscountType().trim().toUpperCase(); - if ("PERCENTAGE".equals(type) || "PERCENT".equals(type)) { - discount = subtotal.multiply(coupon.getDiscountValue().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP)); - } else if ("FIXED".equals(type)) { - discount = coupon.getDiscountValue(); - } - - return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP); - } - private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) { if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) { return BigDecimal.ZERO; @@ -419,14 +383,6 @@ public class SaleService { return response; } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - String normalizePaymentMethod(String paymentMethod) { if (paymentMethod == null) { return null; diff --git a/backend/src/main/java/com/petshop/backend/service/UserService.java b/backend/src/main/java/com/petshop/backend/service/UserService.java index 75966114..4fe91a26 100644 --- a/backend/src/main/java/com/petshop/backend/service/UserService.java +++ b/backend/src/main/java/com/petshop/backend/service/UserService.java @@ -10,6 +10,7 @@ import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.security.UserAuthCacheService; import com.petshop.backend.util.AuthenticationHelper; +import com.petshop.backend.util.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.security.access.AccessDeniedException; @@ -81,7 +82,7 @@ public class UserService { requireRequestedRole(request.getRole(), requiredRole); User user = new User(); - user.setUsername(trimToNull(request.getUsername())); + user.setUsername(StringUtils.trimToNull(request.getUsername())); if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) { user.setPassword(passwordEncoder.encode(request.getPassword())); } @@ -89,9 +90,9 @@ public class UserService { user.setLastName(request.getLastName()); user.setFullName(request.getFullName()); user.setEmail(request.getEmail()); - user.setPhone(trimToNull(request.getPhone())); + user.setPhone(StringUtils.trimToNull(request.getPhone())); user.setRole(request.getRole()); - user.setStaffRole(trimToNull(request.getStaffRole())); + user.setStaffRole(StringUtils.trimToNull(request.getStaffRole())); user.setPrimaryStore(resolveStore(request.getPrimaryStoreId())); user.setActive(request.getActive() != null ? request.getActive() : true); if (request.getLoyaltyPoints() != null) { @@ -124,7 +125,7 @@ public class UserService { || user.getRole() != request.getRole() || !user.getActive().equals(request.getActive() != null ? request.getActive() : true); - user.setUsername(trimToNull(request.getUsername())); + user.setUsername(StringUtils.trimToNull(request.getUsername())); if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) { user.setPassword(passwordEncoder.encode(request.getPassword())); invalidateToken = true; @@ -133,13 +134,13 @@ public class UserService { user.setLastName(request.getLastName()); user.setFullName(request.getFullName()); user.setEmail(request.getEmail()); - String phone = trimToNull(request.getPhone()); + String phone = StringUtils.trimToNull(request.getPhone()); if (!Objects.equals(user.getPhone(), phone)) { validateUniquePhone(phone, user.getId()); } user.setPhone(phone); user.setRole(request.getRole()); - user.setStaffRole(trimToNull(request.getStaffRole())); + user.setStaffRole(StringUtils.trimToNull(request.getStaffRole())); user.setPrimaryStore(resolveStore(request.getPrimaryStoreId())); user.setActive(request.getActive() != null ? request.getActive() : true); if (request.getLoyaltyPoints() != null) { @@ -277,16 +278,8 @@ public class UserService { }); } - private String trimToNull(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private User.Role parseRole(String role) { - String normalizedRole = trimToNull(role); + String normalizedRole = StringUtils.trimToNull(role); if (normalizedRole == null) { return null; } diff --git a/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java index c78e87d3..4586ab2d 100644 --- a/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java +++ b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java @@ -33,6 +33,21 @@ public class AuthenticationHelper { return getAuthenticatedPrincipal().getUserId(); } + public static User.Role getAuthenticatedRole() { + return getAuthenticatedPrincipal().getRole(); + } + + public static boolean isCustomer() { + return getAuthenticatedPrincipal().getRole() == User.Role.CUSTOMER; + } + + public static Long getCustomerIdOrNull(UserRepository userRepository) { + if (!isCustomer()) { + return null; + } + return getAuthenticatedUser(userRepository).getId(); + } + public static User getAuthenticatedUser(UserRepository userRepository) { Authentication authentication = getAuthentication(); Object principal = authentication.getPrincipal(); diff --git a/backend/src/main/java/com/petshop/backend/util/ImageValidationUtil.java b/backend/src/main/java/com/petshop/backend/util/ImageValidationUtil.java new file mode 100644 index 00000000..30214fb4 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/util/ImageValidationUtil.java @@ -0,0 +1,30 @@ +package com.petshop.backend.util; + +import com.petshop.backend.exception.BusinessException; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Locale; + +public final class ImageValidationUtil { + + public static final long MAX_IMAGE_SIZE = 5 * 1024 * 1024; + + private ImageValidationUtil() {} + + public static void validate(MultipartFile file) { + if (file.isEmpty()) { + throw new BusinessException("Please select an image to upload"); + } + if (file.getSize() > MAX_IMAGE_SIZE) { + throw new BusinessException("Image file size must be less than 5MB"); + } + String contentType = file.getContentType(); + if (contentType == null) { + throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); + } + String normalized = contentType.toLowerCase(Locale.ROOT); + if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) { + throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); + } + } +} diff --git a/backend/src/main/java/com/petshop/backend/util/StringUtils.java b/backend/src/main/java/com/petshop/backend/util/StringUtils.java new file mode 100644 index 00000000..625c294d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/util/StringUtils.java @@ -0,0 +1,14 @@ +package com.petshop.backend.util; + +public final class StringUtils { + + private StringUtils() {} + + public static String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } +} From 80df6116ab57a3f9ea569614430edbb258a6c9f8 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 14:18:00 -0600 Subject: [PATCH 04/14] consolidate shared constants --- .../com/petshop/backend/service/CartService.java | 3 ++- .../com/petshop/backend/service/PetService.java | 15 +++------------ .../petshop/backend/service/ProductService.java | 15 +++------------ .../com/petshop/backend/service/SaleService.java | 5 +++-- .../petshop/backend/util/BusinessConstants.java | 11 +++++++++++ 5 files changed, 22 insertions(+), 27 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/util/BusinessConstants.java diff --git a/backend/src/main/java/com/petshop/backend/service/CartService.java b/backend/src/main/java/com/petshop/backend/service/CartService.java index 9e11ac13..47e411e6 100644 --- a/backend/src/main/java/com/petshop/backend/service/CartService.java +++ b/backend/src/main/java/com/petshop/backend/service/CartService.java @@ -7,6 +7,7 @@ import com.petshop.backend.entity.*; import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.*; +import com.petshop.backend.util.BusinessConstants; import com.stripe.Stripe; import com.stripe.exception.StripeException; import com.stripe.model.PaymentIntent; @@ -24,7 +25,7 @@ import java.util.List; @Service public class CartService { - private static final int LOYALTY_POINTS_PER_DOLLAR = 20; + private static final int LOYALTY_POINTS_PER_DOLLAR = BusinessConstants.LOYALTY_POINTS_PER_DOLLAR; private final CartRepository cartRepository; private final CartItemRepository cartItemRepository; 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 98b63cd8..2f90011a 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.util.ImageValidationUtil; import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.pet.MyPetRequest; import com.petshop.backend.dto.pet.MyPetResponse; @@ -31,7 +32,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.List; -import java.util.Locale; + @Service public class PetService { @@ -284,17 +285,7 @@ public class PetService { if (file == null || file.isEmpty()) { throw new BusinessException("Please select an image to upload"); } - if (file.getSize() > 5 * 1024 * 1024) { - throw new BusinessException("Image file size must be less than 5MB"); - } - String contentType = file.getContentType(); - if (contentType == null) { - throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); - } - String normalized = contentType.toLowerCase(Locale.ROOT); - if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) { - throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); - } + ImageValidationUtil.validate(file); } private Pet findOwnedPet(Long ownerUserId, Long petId) { diff --git a/backend/src/main/java/com/petshop/backend/service/ProductService.java b/backend/src/main/java/com/petshop/backend/service/ProductService.java index 9f2a9069..fc292f36 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.util.ImageValidationUtil; import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.product.ProductRequest; import com.petshop.backend.dto.product.ProductResponse; @@ -21,7 +22,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; -import java.util.Locale; + @Service public class ProductService { @@ -138,17 +139,7 @@ public class ProductService { if (file == null || file.isEmpty()) { throw new BusinessException("Please select an image to upload"); } - if (file.getSize() > 5 * 1024 * 1024) { - throw new BusinessException("Image file size must be less than 5MB"); - } - String contentType = file.getContentType(); - if (contentType == null) { - throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); - } - String normalized = contentType.toLowerCase(Locale.ROOT); - if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) { - throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); - } + ImageValidationUtil.validate(file); } private void deleteStoredImageIfPresent(String storedImagePath) { diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index f69ef9e1..adf6ab3b 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -8,6 +8,7 @@ import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.event.SaleReceiptEvent; import com.petshop.backend.repository.*; import com.petshop.backend.util.AuthenticationHelper; +import com.petshop.backend.util.BusinessConstants; import com.petshop.backend.util.StringUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; @@ -24,8 +25,8 @@ import java.util.List; @Service public class SaleService { - private static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = new BigDecimal("0.10"); - private static final int LOYALTY_POINTS_PER_DOLLAR = 20; + private static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = BusinessConstants.EMPLOYEE_DISCOUNT_PERCENT; + private static final int LOYALTY_POINTS_PER_DOLLAR = BusinessConstants.LOYALTY_POINTS_PER_DOLLAR; private final SaleRepository saleRepository; private final ProductRepository productRepository; diff --git a/backend/src/main/java/com/petshop/backend/util/BusinessConstants.java b/backend/src/main/java/com/petshop/backend/util/BusinessConstants.java new file mode 100644 index 00000000..0be56b8a --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/util/BusinessConstants.java @@ -0,0 +1,11 @@ +package com.petshop.backend.util; + +import java.math.BigDecimal; + +public final class BusinessConstants { + + private BusinessConstants() {} + + public static final int LOYALTY_POINTS_PER_DOLLAR = 20; + public static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = new BigDecimal("0.10"); +} From d198fb3d4211a2178ba40ad1094f49d425f1c540 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 14:43:00 -0600 Subject: [PATCH 05/14] externalize business constants --- .../petshop/backend/BackendApplication.java | 3 +++ .../backend/config/BusinessProperties.java | 25 +++++++++++++++++++ .../backend/service/AppointmentService.java | 11 +++++--- .../petshop/backend/service/CartService.java | 11 ++++---- .../petshop/backend/service/SaleService.java | 17 ++++++------- .../backend/util/BusinessConstants.java | 11 -------- backend/src/main/resources/application.yml | 9 +++++++ 7 files changed, 58 insertions(+), 29 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/config/BusinessProperties.java delete mode 100644 backend/src/main/java/com/petshop/backend/util/BusinessConstants.java diff --git a/backend/src/main/java/com/petshop/backend/BackendApplication.java b/backend/src/main/java/com/petshop/backend/BackendApplication.java index 26d18f3c..584a7fd0 100644 --- a/backend/src/main/java/com/petshop/backend/BackendApplication.java +++ b/backend/src/main/java/com/petshop/backend/BackendApplication.java @@ -1,13 +1,16 @@ package com.petshop.backend; +import com.petshop.backend.config.BusinessProperties; import com.petshop.backend.config.FlywayContextInitializer; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.data.web.config.EnableSpringDataWebSupport; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication +@EnableConfigurationProperties(BusinessProperties.class) @EnableScheduling @EnableAsync @EnableSpringDataWebSupport(pageSerializationMode = EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO) diff --git a/backend/src/main/java/com/petshop/backend/config/BusinessProperties.java b/backend/src/main/java/com/petshop/backend/config/BusinessProperties.java new file mode 100644 index 00000000..6d1cee18 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/config/BusinessProperties.java @@ -0,0 +1,25 @@ +package com.petshop.backend.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.math.BigDecimal; +import java.time.LocalTime; + +@ConfigurationProperties(prefix = "petshop.business") +public record BusinessProperties( + LocalTime openTime, + LocalTime closeTime, + int slotIntervalMinutes, + long maxImageSizeBytes, + BigDecimal employeeDiscountPercent, + int loyaltyPointsPerDollar +) { + public BusinessProperties { + if (openTime == null) openTime = LocalTime.of(9, 0); + if (closeTime == null) closeTime = LocalTime.of(17, 0); + if (slotIntervalMinutes <= 0) slotIntervalMinutes = 30; + if (maxImageSizeBytes <= 0) maxImageSizeBytes = 5 * 1024 * 1024; + if (employeeDiscountPercent == null) employeeDiscountPercent = new BigDecimal("0.10"); + if (loyaltyPointsPerDollar <= 0) loyaltyPointsPerDollar = 20; + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 59f290a8..34e6da18 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -19,6 +19,7 @@ import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.config.BusinessProperties; import com.petshop.backend.util.AuthenticationHelper; import com.petshop.backend.util.StringUtils; import org.springframework.context.ApplicationEventPublisher; @@ -47,8 +48,9 @@ public class AppointmentService { private final UserRepository userRepository; private final AdoptionRepository adoptionRepository; private final ApplicationEventPublisher eventPublisher; + private final BusinessProperties businessProperties; - public AppointmentService(AppointmentRepository appointmentRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, AdoptionRepository adoptionRepository, ApplicationEventPublisher eventPublisher) { + public AppointmentService(AppointmentRepository appointmentRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, AdoptionRepository adoptionRepository, ApplicationEventPublisher eventPublisher, BusinessProperties businessProperties) { this.appointmentRepository = appointmentRepository; this.serviceRepository = serviceRepository; this.petRepository = petRepository; @@ -56,6 +58,7 @@ public class AppointmentService { this.userRepository = userRepository; this.adoptionRepository = adoptionRepository; this.eventPublisher = eventPublisher; + this.businessProperties = businessProperties; } @Transactional(readOnly = true) @@ -240,8 +243,8 @@ public class AppointmentService { .collect(Collectors.groupingBy(a -> a.getEmployee().getId())); List availableSlots = new ArrayList<>(); - LocalTime startTime = LocalTime.of(9, 0); - LocalTime endTime = LocalTime.of(17, 0); + LocalTime startTime = businessProperties.openTime(); + LocalTime endTime = businessProperties.closeTime(); LocalTime latestStart = endTime.minusMinutes(service.getServiceDuration()); LocalTime currentTime = startTime; @@ -255,7 +258,7 @@ public class AppointmentService { if (anyEmployeeAvailable) { availableSlots.add(currentTime.toString()); } - currentTime = currentTime.plusMinutes(30); + currentTime = currentTime.plusMinutes(businessProperties.slotIntervalMinutes()); } return availableSlots; diff --git a/backend/src/main/java/com/petshop/backend/service/CartService.java b/backend/src/main/java/com/petshop/backend/service/CartService.java index 47e411e6..305d5ef4 100644 --- a/backend/src/main/java/com/petshop/backend/service/CartService.java +++ b/backend/src/main/java/com/petshop/backend/service/CartService.java @@ -7,7 +7,7 @@ import com.petshop.backend.entity.*; import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.*; -import com.petshop.backend.util.BusinessConstants; +import com.petshop.backend.config.BusinessProperties; import com.stripe.Stripe; import com.stripe.exception.StripeException; import com.stripe.model.PaymentIntent; @@ -25,8 +25,6 @@ import java.util.List; @Service public class CartService { - private static final int LOYALTY_POINTS_PER_DOLLAR = BusinessConstants.LOYALTY_POINTS_PER_DOLLAR; - private final CartRepository cartRepository; private final CartItemRepository cartItemRepository; private final UserRepository userRepository; @@ -36,6 +34,7 @@ public class CartService { private final CouponService couponService; private final SaleRepository saleRepository; private final SaleService saleService; + private final BusinessProperties businessProperties; @Value("${stripe.secret-key:}") private String stripeSecretKey; @@ -48,7 +47,8 @@ public class CartService { CouponRepository couponRepository, CouponService couponService, SaleRepository saleRepository, - SaleService saleService) { + SaleService saleService, + BusinessProperties businessProperties) { this.cartRepository = cartRepository; this.cartItemRepository = cartItemRepository; this.userRepository = userRepository; @@ -58,6 +58,7 @@ public class CartService { this.couponService = couponService; this.saleRepository = saleRepository; this.saleService = saleService; + this.businessProperties = businessProperties; } @PostConstruct @@ -473,7 +474,7 @@ public class CartService { } int availablePoints = user.getLoyaltyPoints() != null ? user.getLoyaltyPoints() : 0; - int wholeDollars = availablePoints / LOYALTY_POINTS_PER_DOLLAR; + int wholeDollars = availablePoints / businessProperties.loyaltyPointsPerDollar(); if (wholeDollars <= 0) { return BigDecimal.ZERO; } diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index adf6ab3b..f23796e7 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -8,7 +8,7 @@ import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.event.SaleReceiptEvent; import com.petshop.backend.repository.*; import com.petshop.backend.util.AuthenticationHelper; -import com.petshop.backend.util.BusinessConstants; +import com.petshop.backend.config.BusinessProperties; import com.petshop.backend.util.StringUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; @@ -25,9 +25,6 @@ import java.util.List; @Service public class SaleService { - private static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = BusinessConstants.EMPLOYEE_DISCOUNT_PERCENT; - private static final int LOYALTY_POINTS_PER_DOLLAR = BusinessConstants.LOYALTY_POINTS_PER_DOLLAR; - private final SaleRepository saleRepository; private final ProductRepository productRepository; private final StoreRepository storeRepository; @@ -37,8 +34,9 @@ public class SaleService { private final CouponService couponService; private final CartRepository cartRepository; private final ApplicationEventPublisher eventPublisher; + private final BusinessProperties businessProperties; - public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CouponService couponService, CartRepository cartRepository, ApplicationEventPublisher eventPublisher) { + public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CouponService couponService, CartRepository cartRepository, ApplicationEventPublisher eventPublisher, BusinessProperties businessProperties) { this.saleRepository = saleRepository; this.productRepository = productRepository; this.storeRepository = storeRepository; @@ -48,6 +46,7 @@ public class SaleService { this.couponService = couponService; this.cartRepository = cartRepository; this.eventPublisher = eventPublisher; + this.businessProperties = businessProperties; } @Transactional(readOnly = true) @@ -251,7 +250,7 @@ public class SaleService { int pointsDeducted; if (request.getPointsUsed() != null && request.getPointsUsed() > 0) { loyaltyDiscount = BigDecimal.valueOf(request.getPointsUsed()) - .divide(BigDecimal.valueOf(LOYALTY_POINTS_PER_DOLLAR), 2, RoundingMode.HALF_UP) + .divide(BigDecimal.valueOf(businessProperties.loyaltyPointsPerDollar()), 2, RoundingMode.HALF_UP) .min(remainingAfterDiscounts.max(BigDecimal.ZERO)) .setScale(2, RoundingMode.HALF_UP); pointsDeducted = request.getPointsUsed(); @@ -292,7 +291,7 @@ public class SaleService { } if (customer.getRole() == User.Role.STAFF || customer.getRole() == User.Role.ADMIN) { - return remainingAmount.multiply(EMPLOYEE_DISCOUNT_PERCENT).setScale(2, RoundingMode.HALF_UP); + return remainingAmount.multiply(businessProperties.employeeDiscountPercent()).setScale(2, RoundingMode.HALF_UP); } return BigDecimal.ZERO; @@ -304,7 +303,7 @@ public class SaleService { } int availablePoints = customer.getLoyaltyPoints() != null ? customer.getLoyaltyPoints() : 0; - int wholeDollars = availablePoints / LOYALTY_POINTS_PER_DOLLAR; + int wholeDollars = availablePoints / businessProperties.loyaltyPointsPerDollar(); if (wholeDollars <= 0) { return BigDecimal.ZERO; } @@ -319,7 +318,7 @@ public class SaleService { if (loyaltyDiscount == null || loyaltyDiscount.compareTo(BigDecimal.ZERO) <= 0) { return 0; } - return loyaltyDiscount.setScale(0, RoundingMode.DOWN).intValue() * LOYALTY_POINTS_PER_DOLLAR; + return loyaltyDiscount.setScale(0, RoundingMode.DOWN).intValue() * businessProperties.loyaltyPointsPerDollar(); } private User resolveWebsiteSaleEmployee(Long storeId) { diff --git a/backend/src/main/java/com/petshop/backend/util/BusinessConstants.java b/backend/src/main/java/com/petshop/backend/util/BusinessConstants.java deleted file mode 100644 index 0be56b8a..00000000 --- a/backend/src/main/java/com/petshop/backend/util/BusinessConstants.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.petshop.backend.util; - -import java.math.BigDecimal; - -public final class BusinessConstants { - - private BusinessConstants() {} - - public static final int LOYALTY_POINTS_PER_DOLLAR = 20; - public static final BigDecimal EMPLOYEE_DISCOUNT_PERCENT = new BigDecimal("0.10"); -} diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index e3256221..20773f86 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -61,6 +61,15 @@ app: frontend-url: ${FRONTEND_URL:http://localhost:3000} allowed-origins: ${ALLOWED_ORIGINS:http://localhost:3000,http://localhost:3001,http://127.0.0.1:3000,https://petshop-web.nicepond-c7280126.westus2.azurecontainerapps.io} +petshop: + business: + open-time: "09:00" + close-time: "17:00" + slot-interval-minutes: 30 + max-image-size-bytes: 5242880 + employee-discount-percent: 0.10 + loyalty-points-per-dollar: 20 + azure: storage: connection-string: ${AZURE_STORAGE_CONNECTION_STRING:} From 287e71f2a9420ed844081e75134f34abeea054d8 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 15:12:00 -0600 Subject: [PATCH 06/14] fix review findings --- .../backend/controller/AuthController.java | 14 +---- .../controller/UserAvatarController.java | 58 +++++++------------ .../backend/service/CouponService.java | 4 +- .../backend/util/ImageValidationUtil.java | 8 ++- 4 files changed, 30 insertions(+), 54 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index ad1e9be4..7bdb53a1 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -24,6 +24,7 @@ import com.petshop.backend.service.AvatarStorageService; import com.petshop.backend.service.EmailService; import com.petshop.backend.service.PasswordResetService; import com.petshop.backend.util.AuthenticationHelper; +import com.petshop.backend.util.ImageValidationUtil; import com.petshop.backend.util.PhoneUtils; import com.petshop.backend.util.StringUtils; import jakarta.validation.Valid; @@ -296,18 +297,7 @@ public class AuthController { public ResponseEntity uploadAvatar(@RequestParam("avatar") MultipartFile file) { User user = getAuthenticatedUser(); - if (file.isEmpty()) { - throw new BusinessException("Please select a file to upload"); - } - - if (file.getSize() > 5 * 1024 * 1024) { - throw new BusinessException("File size must not exceed 5MB"); - } - - String contentType = file.getContentType(); - if (contentType == null || (!contentType.equals("image/jpeg") && !contentType.equals("image/png") && !contentType.equals("image/gif"))) { - throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); - } + ImageValidationUtil.validate(file); try { avatarStorageService.deleteAvatar(user); diff --git a/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java b/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java index 68ef41ef..602cfe54 100644 --- a/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java +++ b/backend/src/main/java/com/petshop/backend/controller/UserAvatarController.java @@ -1,10 +1,12 @@ package com.petshop.backend.controller; import com.petshop.backend.entity.User; +import com.petshop.backend.exception.BusinessException; +import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AvatarStorageService; +import com.petshop.backend.util.ImageValidationUtil; import org.springframework.core.io.Resource; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.DeleteMapping; @@ -15,6 +17,8 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; import java.util.HashMap; @@ -24,6 +28,8 @@ import java.util.Map; @RequestMapping("/api/v1/users") public class UserAvatarController { + private static final Logger log = LoggerFactory.getLogger(UserAvatarController.class); + private final UserRepository userRepository; private final AvatarStorageService avatarStorageService; @@ -53,29 +59,10 @@ public class UserAvatarController { @PostMapping("/{userId}/avatar") @PreAuthorize("hasRole('ADMIN')") public ResponseEntity uploadUserAvatar(@PathVariable Long userId, @RequestParam("avatar") MultipartFile file) { - User user = userRepository.findById(userId).orElse(null); - if (user == null) { - return ResponseEntity.notFound().build(); - } + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); - if (file.isEmpty()) { - Map error = new HashMap<>(); - error.put("message", "Please select a file to upload"); - return ResponseEntity.badRequest().body(error); - } - - if (file.getSize() > 5 * 1024 * 1024) { - Map error = new HashMap<>(); - error.put("message", "File size must not exceed 5MB"); - return ResponseEntity.badRequest().body(error); - } - - String contentType = file.getContentType(); - if (contentType == null || (!contentType.equals("image/jpeg") && !contentType.equals("image/png") && !contentType.equals("image/gif"))) { - Map error = new HashMap<>(); - error.put("message", "Only JPG, PNG, and GIF images are allowed"); - return ResponseEntity.badRequest().body(error); - } + ImageValidationUtil.validate(file); try { avatarStorageService.deleteAvatar(user); @@ -88,31 +75,26 @@ public class UserAvatarController { result.put("message", "Avatar uploaded successfully"); return ResponseEntity.ok(result); } catch (IOException e) { - Map error = new HashMap<>(); - error.put("message", "Failed to upload avatar: " + e.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + throw new BusinessException("Failed to upload avatar: " + e.getMessage()); } } @DeleteMapping("/{userId}/avatar") @PreAuthorize("hasRole('ADMIN')") public ResponseEntity deleteUserAvatar(@PathVariable Long userId) { - User user = userRepository.findById(userId).orElse(null); - if (user == null) { - return ResponseEntity.notFound().build(); - } + User user = userRepository.findById(userId) + .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); try { avatarStorageService.deleteAvatar(user); - user.setAvatarUrl(null); - userRepository.save(user); - Map result = new HashMap<>(); - result.put("message", "Avatar removed successfully"); - return ResponseEntity.ok(result); } catch (IOException e) { - Map error = new HashMap<>(); - error.put("message", "Failed to remove avatar: " + e.getMessage()); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error); + log.warn("Failed to delete avatar for user {}: {}", userId, e.getMessage()); } + user.setAvatarUrl(null); + userRepository.save(user); + + Map result = new HashMap<>(); + result.put("message", "Avatar removed successfully"); + return ResponseEntity.ok(result); } } diff --git a/backend/src/main/java/com/petshop/backend/service/CouponService.java b/backend/src/main/java/com/petshop/backend/service/CouponService.java index eb3b605d..4cb9f295 100644 --- a/backend/src/main/java/com/petshop/backend/service/CouponService.java +++ b/backend/src/main/java/com/petshop/backend/service/CouponService.java @@ -126,8 +126,8 @@ public class CouponService { BigDecimal discount = BigDecimal.ZERO; String type = coupon.getDiscountType(); if (isPercentageType(type)) { - discount = subtotal.multiply(coupon.getDiscountValue()) - .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + discount = subtotal.multiply(coupon.getDiscountValue() + .divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP)); } else if (isFixedType(type)) { discount = coupon.getDiscountValue(); } diff --git a/backend/src/main/java/com/petshop/backend/util/ImageValidationUtil.java b/backend/src/main/java/com/petshop/backend/util/ImageValidationUtil.java index 30214fb4..4ab26d2b 100644 --- a/backend/src/main/java/com/petshop/backend/util/ImageValidationUtil.java +++ b/backend/src/main/java/com/petshop/backend/util/ImageValidationUtil.java @@ -7,15 +7,19 @@ import java.util.Locale; public final class ImageValidationUtil { - public static final long MAX_IMAGE_SIZE = 5 * 1024 * 1024; + public static final long DEFAULT_MAX_IMAGE_SIZE = 5 * 1024 * 1024; private ImageValidationUtil() {} public static void validate(MultipartFile file) { + validate(file, DEFAULT_MAX_IMAGE_SIZE); + } + + public static void validate(MultipartFile file, long maxSizeBytes) { if (file.isEmpty()) { throw new BusinessException("Please select an image to upload"); } - if (file.getSize() > MAX_IMAGE_SIZE) { + if (file.getSize() > maxSizeBytes) { throw new BusinessException("Image file size must be less than 5MB"); } String contentType = file.getContentType(); From ee0a643636ed7421fd46b98bb1c9937526133723 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 15:48:00 -0600 Subject: [PATCH 07/14] clean remaining code smells --- .../controller/PetImageController.java | 57 ++++--------------- .../controller/ProductImageController.java | 19 +------ .../backend/controller/UserController.java | 1 - .../petshop/backend/service/ChatService.java | 3 +- .../backend/service/OpenRouterService.java | 9 +-- 5 files changed, 20 insertions(+), 69 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/PetImageController.java b/backend/src/main/java/com/petshop/backend/controller/PetImageController.java index 93477cc7..2cd9845f 100644 --- a/backend/src/main/java/com/petshop/backend/controller/PetImageController.java +++ b/backend/src/main/java/com/petshop/backend/controller/PetImageController.java @@ -2,14 +2,11 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.pet.PetResponse; import com.petshop.backend.entity.User; -import com.petshop.backend.security.AppPrincipal; import com.petshop.backend.service.PetService; +import com.petshop.backend.util.AuthenticationHelper; import org.springframework.core.io.Resource; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -20,8 +17,6 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; @RestController @RequestMapping("/api/v1/pets") @@ -35,20 +30,20 @@ public class PetImageController { @PostMapping("/{id}/image") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") - public ResponseEntity uploadPetImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) { - try { - PetResponse response = petService.uploadPetImage(id, image); - return ResponseEntity.ok(response); - } catch (IllegalArgumentException ex) { - return badRequest(ex.getMessage()); - } catch (IOException ex) { - return badRequest("Failed to upload pet image: " + ex.getMessage()); - } + public ResponseEntity uploadPetImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) throws IOException { + return ResponseEntity.ok(petService.uploadPetImage(id, image)); } @GetMapping("/{id}/image") public ResponseEntity getPetImage(@PathVariable Long id) { - PetService.ImagePayload payload = petService.loadPetImage(id, currentUserId(), currentUserRole()); + Long userId = null; + User.Role role = null; + try { + userId = AuthenticationHelper.getAuthenticatedUserId(); + role = AuthenticationHelper.getAuthenticatedRole(); + } catch (Exception ignored) { + } + PetService.ImagePayload payload = petService.loadPetImage(id, userId, role); return ResponseEntity.ok().contentType(payload.mediaType()).body(payload.resource()); } @@ -57,34 +52,4 @@ public class PetImageController { public ResponseEntity deletePetImage(@PathVariable Long id) { return ResponseEntity.ok(petService.deletePetImage(id)); } - - private ResponseEntity> badRequest(String message) { - Map error = new HashMap<>(); - error.put("message", message); - return ResponseEntity.badRequest().body(error); - } - - private Long currentUserId() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !authentication.isAuthenticated()) { - return null; - } - Object principal = authentication.getPrincipal(); - if (principal instanceof AppPrincipal appPrincipal) { - return appPrincipal.getUserId(); - } - return null; - } - - private User.Role currentUserRole() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (authentication == null || !authentication.isAuthenticated()) { - return null; - } - Object principal = authentication.getPrincipal(); - if (principal instanceof AppPrincipal appPrincipal) { - return appPrincipal.getRole(); - } - return null; - } } diff --git a/backend/src/main/java/com/petshop/backend/controller/ProductImageController.java b/backend/src/main/java/com/petshop/backend/controller/ProductImageController.java index 015847ad..fb9ffa08 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ProductImageController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ProductImageController.java @@ -15,8 +15,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; -import java.util.HashMap; -import java.util.Map; @RestController @RequestMapping("/api/v1/products") @@ -30,15 +28,8 @@ public class ProductImageController { @PostMapping("/{id}/image") @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") - public ResponseEntity uploadProductImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) { - try { - ProductResponse response = productService.uploadProductImage(id, image); - return ResponseEntity.ok(response); - } catch (IllegalArgumentException ex) { - return badRequest(ex.getMessage()); - } catch (IOException ex) { - return badRequest("Failed to upload product image: " + ex.getMessage()); - } + public ResponseEntity uploadProductImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) throws IOException { + return ResponseEntity.ok(productService.uploadProductImage(id, image)); } @GetMapping("/{id}/image") @@ -52,10 +43,4 @@ public class ProductImageController { public ResponseEntity deleteProductImage(@PathVariable Long id) { return ResponseEntity.ok(productService.deleteProductImage(id)); } - - private ResponseEntity> badRequest(String message) { - Map error = new HashMap<>(); - error.put("message", message); - return ResponseEntity.badRequest().body(error); - } } diff --git a/backend/src/main/java/com/petshop/backend/controller/UserController.java b/backend/src/main/java/com/petshop/backend/controller/UserController.java index 8f7e07c3..28ef9f9e 100644 --- a/backend/src/main/java/com/petshop/backend/controller/UserController.java +++ b/backend/src/main/java/com/petshop/backend/controller/UserController.java @@ -3,7 +3,6 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.dto.user.UserRequest; import com.petshop.backend.dto.user.UserResponse; -import com.petshop.backend.entity.User; import com.petshop.backend.service.UserService; import jakarta.validation.Valid; import org.springframework.data.domain.Page; diff --git a/backend/src/main/java/com/petshop/backend/service/ChatService.java b/backend/src/main/java/com/petshop/backend/service/ChatService.java index 620f6130..a1442d41 100644 --- a/backend/src/main/java/com/petshop/backend/service/ChatService.java +++ b/backend/src/main/java/com/petshop/backend/service/ChatService.java @@ -8,6 +8,7 @@ import com.petshop.backend.dto.chat.UpdateConversationRequest; import com.petshop.backend.entity.Conversation; import com.petshop.backend.entity.Message; import com.petshop.backend.entity.User; +import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.util.ContentFilter; import com.petshop.backend.repository.ConversationRepository; @@ -229,7 +230,7 @@ public class ChatService { return toMessageResponse(message); } catch (IOException e) { - throw new RuntimeException("Failed to store attachment", e); + throw new BusinessException("Failed to store attachment"); } } diff --git a/backend/src/main/java/com/petshop/backend/service/OpenRouterService.java b/backend/src/main/java/com/petshop/backend/service/OpenRouterService.java index a70ece30..c3314994 100644 --- a/backend/src/main/java/com/petshop/backend/service/OpenRouterService.java +++ b/backend/src/main/java/com/petshop/backend/service/OpenRouterService.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.petshop.backend.dto.ai.AiChatRequest; import com.petshop.backend.entity.Pet; +import com.petshop.backend.exception.BusinessException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -52,7 +53,7 @@ public class OpenRouterService { HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { - throw new RuntimeException("OpenRouter API returned status " + response.statusCode() + ": " + response.body()); + throw new BusinessException("OpenRouter API returned status " + response.statusCode() + ": " + response.body()); } return extractContent(response.body()); @@ -60,10 +61,10 @@ public class OpenRouterService { catch (InterruptedException e) { Thread.currentThread().interrupt(); - throw new RuntimeException("AI request was interrupted", e); + throw new BusinessException("AI request was interrupted"); } catch (Exception e) { - throw new RuntimeException("Failed to call OpenRouter API: " + e.getMessage(), e); + throw new BusinessException("Failed to call OpenRouter API: " + e.getMessage()); } } @@ -132,6 +133,6 @@ public class OpenRouterService { return choices.get(0).path("message").path("content").asText(); } - throw new RuntimeException("No content in OpenRouter response: " + responseBody); + throw new BusinessException("No content in OpenRouter response"); } } From 4f73da121811bd0de81e9b31a6e0d16cfc8ffd6a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 16:15:00 -0600 Subject: [PATCH 08/14] centralize StringUtils usage --- .../backend/controller/AuthController.java | 27 ++++------------- .../controller/DropdownController.java | 7 +++-- .../backend/service/ActivityLogService.java | 29 ++++++------------- .../backend/service/AdoptionService.java | 4 +-- .../backend/service/AnalyticsService.java | 5 ++-- .../backend/service/AppointmentService.java | 4 +-- .../petshop/backend/service/EmailService.java | 3 +- .../petshop/backend/service/PetService.java | 2 +- .../petshop/backend/service/SaleService.java | 4 +-- .../petshop/backend/service/UserService.java | 4 +-- 10 files changed, 32 insertions(+), 57 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index 7bdb53a1..b0b1ffa5 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -25,7 +25,6 @@ import com.petshop.backend.service.EmailService; import com.petshop.backend.service.PasswordResetService; import com.petshop.backend.util.AuthenticationHelper; import com.petshop.backend.util.ImageValidationUtil; -import com.petshop.backend.util.PhoneUtils; import com.petshop.backend.util.StringUtils; import jakarta.validation.Valid; import org.springframework.core.io.Resource; @@ -84,7 +83,7 @@ public class AuthController { String email = StringUtils.trimToNull(request.getEmail()); String firstName = StringUtils.trimToNull(request.getFirstName()); String lastName = StringUtils.trimToNull(request.getLastName()); - String phone = normalizePhone(request.getPhone()); + String phone = StringUtils.normalizePhone(request.getPhone()); if (userRepository.findByUsername(username).isPresent()) { log.warn("Registration rejected: username already exists ({})", username); @@ -107,7 +106,7 @@ public class AuthController { user.setEmail(email); user.setFirstName(firstName); user.setLastName(lastName); - user.setFullName(joinFullName(firstName, lastName)); + user.setFullName(StringUtils.fullName(firstName, lastName)); user.setPhone(phone); user.setRole(User.Role.CUSTOMER); user.setActive(true); @@ -219,11 +218,11 @@ public class AuthController { user.setLastName(lastName); } if (firstName != null || lastName != null) { - user.setFullName(joinFullName(user.getFirstName(), user.getLastName())); + user.setFullName(StringUtils.fullName(user.getFirstName(), user.getLastName())); } if (request.getPhone() != null) { - String phone = normalizePhone(request.getPhone()); + String phone = StringUtils.normalizePhone(request.getPhone()); if (!java.util.Objects.equals(phone, user.getPhone())) { if (phone != null && userRepository.findByPhone(phone) .filter(existing -> !existing.getId().equals(user.getId())) @@ -258,7 +257,7 @@ public class AuthController { Long customerId = user.getRole() == User.Role.CUSTOMER ? user.getId() : null; String fullName = user.getFullName(); if (fullName == null || fullName.isBlank()) { - fullName = joinFullName(user.getFirstName(), user.getLastName()); + fullName = StringUtils.fullName(user.getFirstName(), user.getLastName()); } return new UserInfoResponse( user.getId(), @@ -277,22 +276,6 @@ public class AuthController { ); } - private String normalizePhone(String value) { - return StringUtils.trimToNull(PhoneUtils.normalize(StringUtils.trimToNull(value))); - } - - private String joinFullName(String firstName, String lastName) { - String first = StringUtils.trimToNull(firstName); - String last = StringUtils.trimToNull(lastName); - if (first == null) { - return last == null ? null : last; - } - if (last == null) { - return first; - } - return first + " " + last; - } - @PostMapping("/me/avatar") public ResponseEntity uploadAvatar(@RequestParam("avatar") MultipartFile file) { User user = getAuthenticatedUser(); diff --git a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java index 5ca97986..756a9fca 100644 --- a/backend/src/main/java/com/petshop/backend/controller/DropdownController.java +++ b/backend/src/main/java/com/petshop/backend/controller/DropdownController.java @@ -3,6 +3,7 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.common.DropdownOption; import com.petshop.backend.entity.User; import com.petshop.backend.repository.*; +import com.petshop.backend.util.StringUtils; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -68,7 +69,7 @@ public class DropdownController { public ResponseEntity> getCustomers() { return ResponseEntity.ok( userRepository.findByRoleAndActiveTrue(User.Role.CUSTOMER).stream() - .map(u -> new DropdownOption(u.getId(), u.getFirstName() + " " + u.getLastName())) + .map(u -> new DropdownOption(u.getId(), StringUtils.fullName(u.getFirstName(), u.getLastName()))) .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); @@ -90,7 +91,7 @@ public class DropdownController { return ResponseEntity.ok( userRepository.findByRoleAndActiveTrue(User.Role.CUSTOMER).stream() .filter(u -> customersWithPets.contains(u.getId())) - .map(u -> new DropdownOption(u.getId(), u.getFirstName() + " " + u.getLastName())) + .map(u -> new DropdownOption(u.getId(), StringUtils.fullName(u.getFirstName(), u.getLastName()))) .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); @@ -190,7 +191,7 @@ public class DropdownController { } return ResponseEntity.ok( employees.stream() - .map(u -> new DropdownOption(u.getId(), u.getFirstName() + " " + u.getLastName())) + .map(u -> new DropdownOption(u.getId(), StringUtils.fullName(u.getFirstName(), u.getLastName()))) .sorted(Comparator.comparing(DropdownOption::getLabel, String.CASE_INSENSITIVE_ORDER)) .collect(Collectors.toList()) ); diff --git a/backend/src/main/java/com/petshop/backend/service/ActivityLogService.java b/backend/src/main/java/com/petshop/backend/service/ActivityLogService.java index a3880071..598d9f95 100644 --- a/backend/src/main/java/com/petshop/backend/service/ActivityLogService.java +++ b/backend/src/main/java/com/petshop/backend/service/ActivityLogService.java @@ -6,6 +6,7 @@ import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.repository.ActivityLogRepository; import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.util.StringUtils; import jakarta.persistence.criteria.Predicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,7 +49,10 @@ public class ActivityLogService { entry.setUser(managedUser); entry.setStore(store); entry.setUsernameSnapshot(managedUser.getUsername()); - entry.setFullNameSnapshot(resolveFullName(managedUser)); + String fullName = managedUser.getFullName() != null && !managedUser.getFullName().isBlank() + ? managedUser.getFullName() + : StringUtils.fullName(managedUser.getFirstName(), managedUser.getLastName()); + entry.setFullNameSnapshot(fullName); entry.setRoleSnapshot(managedUser.getRole() != null ? managedUser.getRole().name() : null); entry.setStoreNameSnapshot(store != null ? store.getStoreName() : null); entry.setActivity(activity.trim()); @@ -127,7 +131,10 @@ public class ActivityLogService { if (entry.getUser() != null) { response.setUserId(entry.getUser().getId()); response.setUsername(firstNonBlank(entry.getUsernameSnapshot(), entry.getUser().getUsername())); - response.setFullName(firstNonBlank(entry.getFullNameSnapshot(), resolveFullName(entry.getUser()))); + String liveName = entry.getUser().getFullName() != null && !entry.getUser().getFullName().isBlank() + ? entry.getUser().getFullName() + : StringUtils.fullName(entry.getUser().getFirstName(), entry.getUser().getLastName()); + response.setFullName(firstNonBlank(entry.getFullNameSnapshot(), liveName)); response.setRole(firstNonBlank(entry.getRoleSnapshot(), entry.getUser().getRole() != null ? entry.getUser().getRole().name() : null)); } @@ -147,24 +154,6 @@ public class ActivityLogService { return response; } - private String resolveFullName(User user) { - if (user == null) { - return null; - } - if (user.getFullName() != null && !user.getFullName().isBlank()) { - return user.getFullName(); - } - String first = user.getFirstName(); - String last = user.getLastName(); - if (first == null || first.isBlank()) { - return last; - } - if (last == null || last.isBlank()) { - return first; - } - return first.trim() + " " + last.trim(); - } - private String firstNonBlank(String preferred, String fallback) { if (preferred != null && !preferred.isBlank()) { return preferred; diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 679aa18c..6a6c32e9 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -256,9 +256,9 @@ public class AdoptionService { adoption.getPet().getPetId(), adoption.getPet().getPetName(), adoption.getCustomer().getId(), - adoption.getCustomer().getFirstName() + " " + adoption.getCustomer().getLastName(), + StringUtils.fullName(adoption.getCustomer().getFirstName(), adoption.getCustomer().getLastName()), adoption.getEmployee().getId(), - adoption.getEmployee().getFirstName() + " " + adoption.getEmployee().getLastName(), + StringUtils.fullName(adoption.getEmployee().getFirstName(), adoption.getEmployee().getLastName()), sourceStore != null ? sourceStore.getStoreId() : null, sourceStore != null ? sourceStore.getStoreName() : null, adoption.getAdoptionDate(), diff --git a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java index 7a69357b..eae169d0 100644 --- a/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java +++ b/backend/src/main/java/com/petshop/backend/service/AnalyticsService.java @@ -8,6 +8,7 @@ import com.petshop.backend.entity.User; import com.petshop.backend.repository.InventoryRepository; import com.petshop.backend.repository.ProductRepository; import com.petshop.backend.repository.SaleRepository; +import com.petshop.backend.util.StringUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -181,12 +182,12 @@ public class AnalyticsService { if (sale.getIsRefund()) { continue; } - String employeeName = sale.getEmployee().getFirstName() + " " + sale.getEmployee().getLastName(); + String employeeName = StringUtils.fullName(sale.getEmployee().getFirstName(), sale.getEmployee().getLastName()); employeeRevenue.merge(employeeName, sale.getTotalAmount(), BigDecimal::add); } if (user.getRole() == User.Role.STAFF && employeeRevenue.isEmpty()) { - String employeeName = user.getFirstName() + " " + user.getLastName(); + String employeeName = StringUtils.fullName(user.getFirstName(), user.getLastName()); employeeRevenue.put(employeeName, BigDecimal.ZERO); } diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 34e6da18..5ffcc16d 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -324,7 +324,7 @@ public class AppointmentService { response.setAppointmentId(appointment.getAppointmentId()); if (appointment.getCustomer() != null) { response.setCustomerId(appointment.getCustomer().getId()); - response.setCustomerName(appointment.getCustomer().getFirstName() + " " + appointment.getCustomer().getLastName()); + response.setCustomerName(StringUtils.fullName(appointment.getCustomer().getFirstName(), appointment.getCustomer().getLastName())); } if (appointment.getStore() != null) { response.setStoreId(appointment.getStore().getStoreId()); @@ -339,7 +339,7 @@ public class AppointmentService { response.setAppointmentStatus(appointment.getAppointmentStatus()); if (appointment.getEmployee() != null) { response.setEmployeeId(appointment.getEmployee().getId()); - response.setEmployeeName(appointment.getEmployee().getFirstName() + " " + appointment.getEmployee().getLastName()); + response.setEmployeeName(StringUtils.fullName(appointment.getEmployee().getFirstName(), appointment.getEmployee().getLastName())); } response.setPetName(pet != null ? pet.getPetName() : null); response.setPetId(pet != null ? pet.getPetId() : null); diff --git a/backend/src/main/java/com/petshop/backend/service/EmailService.java b/backend/src/main/java/com/petshop/backend/service/EmailService.java index e7cab2c1..6e096285 100644 --- a/backend/src/main/java/com/petshop/backend/service/EmailService.java +++ b/backend/src/main/java/com/petshop/backend/service/EmailService.java @@ -7,6 +7,7 @@ import com.petshop.backend.entity.Message; import com.petshop.backend.entity.Sale; import com.petshop.backend.entity.SaleItem; import com.petshop.backend.entity.User; +import com.petshop.backend.util.StringUtils; import com.resend.Resend; import com.resend.services.emails.model.CreateEmailOptions; import org.slf4j.Logger; @@ -246,7 +247,7 @@ public class EmailService { String date = appointment.getAppointmentDate() != null ? appointment.getAppointmentDate().format(DATE_FMT) : "—"; String time = appointment.getAppointmentTime() != null ? appointment.getAppointmentTime().format(TIME_FMT) : "—"; String employee = appointment.getEmployee() != null - ? appointment.getEmployee().getFirstName() + " " + appointment.getEmployee().getLastName() : "—"; + ? StringUtils.fullName(appointment.getEmployee().getFirstName(), appointment.getEmployee().getLastName()) : "—"; String pet = appointment.getPet() != null ? appointment.getPet().getPetName() : null; return """
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 2f90011a..e1aacbd2 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -330,7 +330,7 @@ public class PetService { pet.getCreatedAt(), pet.getUpdatedAt(), owner != null ? owner.getId() : null, - owner != null ? owner.getFirstName() + " " + owner.getLastName() : null, + owner != null ? StringUtils.fullName(owner.getFirstName(), owner.getLastName()) : null, store != null ? store.getStoreId() : null, store != null ? store.getStoreName() : null ); diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index f23796e7..c35c07da 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -334,7 +334,7 @@ public class SaleService { if (sale.getEmployee() != null) { response.setEmployeeId(sale.getEmployee().getId()); - response.setEmployeeName(sale.getEmployee().getFirstName() + " " + sale.getEmployee().getLastName()); + response.setEmployeeName(StringUtils.fullName(sale.getEmployee().getFirstName(), sale.getEmployee().getLastName())); } if (sale.getStore() != null) { @@ -344,7 +344,7 @@ public class SaleService { if (sale.getCustomer() != null) { response.setCustomerId(sale.getCustomer().getId()); - response.setCustomerName(sale.getCustomer().getFirstName() + " " + sale.getCustomer().getLastName()); + response.setCustomerName(StringUtils.fullName(sale.getCustomer().getFirstName(), sale.getCustomer().getLastName())); } response.setTotalAmount(sale.getTotalAmount()); diff --git a/backend/src/main/java/com/petshop/backend/service/UserService.java b/backend/src/main/java/com/petshop/backend/service/UserService.java index 4fe91a26..d5cd1243 100644 --- a/backend/src/main/java/com/petshop/backend/service/UserService.java +++ b/backend/src/main/java/com/petshop/backend/service/UserService.java @@ -90,7 +90,7 @@ public class UserService { user.setLastName(request.getLastName()); user.setFullName(request.getFullName()); user.setEmail(request.getEmail()); - user.setPhone(StringUtils.trimToNull(request.getPhone())); + user.setPhone(StringUtils.normalizePhone(request.getPhone())); user.setRole(request.getRole()); user.setStaffRole(StringUtils.trimToNull(request.getStaffRole())); user.setPrimaryStore(resolveStore(request.getPrimaryStoreId())); @@ -134,7 +134,7 @@ public class UserService { user.setLastName(request.getLastName()); user.setFullName(request.getFullName()); user.setEmail(request.getEmail()); - String phone = StringUtils.trimToNull(request.getPhone()); + String phone = StringUtils.normalizePhone(request.getPhone()); if (!Objects.equals(user.getPhone(), phone)) { validateUniquePhone(phone, user.getId()); } From 18030d5d2efa7bed556a1a6d104f55f1afae918f Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 16:40:00 -0600 Subject: [PATCH 09/14] standardize CRUD services --- .../backend/service/CategoryService.java | 2 ++ .../backend/service/ServiceService.java | 35 ++++++++---------- .../petshop/backend/service/StoreService.java | 36 +++++++++---------- .../backend/service/SupplierService.java | 36 +++++++++---------- 4 files changed, 51 insertions(+), 58 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/service/CategoryService.java b/backend/src/main/java/com/petshop/backend/service/CategoryService.java index 742ae847..ea08dc3d 100644 --- a/backend/src/main/java/com/petshop/backend/service/CategoryService.java +++ b/backend/src/main/java/com/petshop/backend/service/CategoryService.java @@ -21,11 +21,13 @@ public class CategoryService { this.categoryRepository = categoryRepository; } + @Transactional(readOnly = true) public Page getAllCategories(String query, String type, Pageable pageable) { return categoryRepository.searchCategories(StringUtils.trimToNull(query), StringUtils.trimToNull(type), pageable) .map(this::mapToResponse); } + @Transactional(readOnly = true) public CategoryResponse getCategoryById(Long id) { Category category = categoryRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + id)); diff --git a/backend/src/main/java/com/petshop/backend/service/ServiceService.java b/backend/src/main/java/com/petshop/backend/service/ServiceService.java index ccd7b829..ecf644c2 100644 --- a/backend/src/main/java/com/petshop/backend/service/ServiceService.java +++ b/backend/src/main/java/com/petshop/backend/service/ServiceService.java @@ -5,6 +5,7 @@ import com.petshop.backend.dto.service.ServiceRequest; import com.petshop.backend.dto.service.ServiceResponse; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.ServiceRepository; +import com.petshop.backend.util.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -21,9 +22,8 @@ public class ServiceService { @Transactional(readOnly = true) public Page getAllServices(String query, String species, Pageable pageable) { - String q = (query != null && !query.trim().isEmpty()) ? query.trim() : null; - String sp = (species != null && !species.trim().isEmpty()) ? species.trim() : null; - return serviceRepository.searchServices(q, sp, pageable).map(this::mapToResponse); + return serviceRepository.searchServices(StringUtils.trimToNull(query), StringUtils.trimToNull(species), pageable) + .map(this::mapToResponse); } @Transactional(readOnly = true) @@ -36,14 +36,7 @@ public class ServiceService { @Transactional public ServiceResponse createService(ServiceRequest request) { com.petshop.backend.entity.Service service = new com.petshop.backend.entity.Service(); - service.setServiceName(request.getServiceName()); - service.setServiceDesc(request.getServiceDesc()); - service.setServicePrice(request.getServicePrice()); - service.setServiceDuration(request.getServiceDuration()); - if (request.getSpecies() != null) { - service.setSpecies(request.getSpecies()); - } - + applyFields(service, request); service = serviceRepository.save(service); return mapToResponse(service); } @@ -52,15 +45,7 @@ public class ServiceService { public ServiceResponse updateService(Long id, ServiceRequest request) { com.petshop.backend.entity.Service service = serviceRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Service not found with id: " + id)); - - service.setServiceName(request.getServiceName()); - service.setServiceDesc(request.getServiceDesc()); - service.setServicePrice(request.getServicePrice()); - service.setServiceDuration(request.getServiceDuration()); - if (request.getSpecies() != null) { - service.setSpecies(request.getSpecies()); - } - + applyFields(service, request); service = serviceRepository.save(service); return mapToResponse(service); } @@ -78,6 +63,16 @@ public class ServiceService { serviceRepository.deleteAllById(request.getIds()); } + private void applyFields(com.petshop.backend.entity.Service service, ServiceRequest request) { + service.setServiceName(request.getServiceName()); + service.setServiceDesc(request.getServiceDesc()); + service.setServicePrice(request.getServicePrice()); + service.setServiceDuration(request.getServiceDuration()); + if (request.getSpecies() != null) { + service.setSpecies(request.getSpecies()); + } + } + private ServiceResponse mapToResponse(com.petshop.backend.entity.Service service) { return new ServiceResponse( service.getServiceId(), diff --git a/backend/src/main/java/com/petshop/backend/service/StoreService.java b/backend/src/main/java/com/petshop/backend/service/StoreService.java index ff9b6581..d9c1bcd3 100644 --- a/backend/src/main/java/com/petshop/backend/service/StoreService.java +++ b/backend/src/main/java/com/petshop/backend/service/StoreService.java @@ -6,6 +6,7 @@ import com.petshop.backend.dto.store.StoreResponse; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.StoreRepository; +import com.petshop.backend.util.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -20,16 +21,16 @@ public class StoreService { this.storeRepository = storeRepository; } + @Transactional(readOnly = true) public Page getAllStores(String query, Pageable pageable) { - Page stores; - if (query != null && !query.trim().isEmpty()) { - stores = storeRepository.searchStores(query, pageable); - } else { - stores = storeRepository.findAll(pageable); - } + String q = StringUtils.trimToNull(query); + Page stores = q != null + ? storeRepository.searchStores(q, pageable) + : storeRepository.findAll(pageable); return stores.map(this::mapToResponse); } + @Transactional(readOnly = true) public StoreResponse getStoreById(Long id) { StoreLocation store = storeRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + id)); @@ -39,12 +40,7 @@ public class StoreService { @Transactional public StoreResponse createStore(StoreRequest request) { StoreLocation store = new StoreLocation(); - store.setStoreName(request.getStoreName()); - store.setAddress(request.getAddress()); - store.setPhone(request.getPhone()); - store.setEmail(request.getEmail()); - store.setImageUrl(request.getImageUrl()); - + applyFields(store, request); store = storeRepository.save(store); return mapToResponse(store); } @@ -53,13 +49,7 @@ public class StoreService { public StoreResponse updateStore(Long id, StoreRequest request) { StoreLocation store = storeRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + id)); - - store.setStoreName(request.getStoreName()); - store.setAddress(request.getAddress()); - store.setPhone(request.getPhone()); - store.setEmail(request.getEmail()); - store.setImageUrl(request.getImageUrl()); - + applyFields(store, request); store = storeRepository.save(store); return mapToResponse(store); } @@ -77,6 +67,14 @@ public class StoreService { storeRepository.deleteAllById(request.getIds()); } + private void applyFields(StoreLocation store, StoreRequest request) { + store.setStoreName(request.getStoreName()); + store.setAddress(request.getAddress()); + store.setPhone(request.getPhone()); + store.setEmail(request.getEmail()); + store.setImageUrl(request.getImageUrl()); + } + private StoreResponse mapToResponse(StoreLocation store) { return new StoreResponse( store.getStoreId(), diff --git a/backend/src/main/java/com/petshop/backend/service/SupplierService.java b/backend/src/main/java/com/petshop/backend/service/SupplierService.java index 2e80eeaa..4bb79b5a 100644 --- a/backend/src/main/java/com/petshop/backend/service/SupplierService.java +++ b/backend/src/main/java/com/petshop/backend/service/SupplierService.java @@ -6,6 +6,7 @@ import com.petshop.backend.dto.supplier.SupplierResponse; import com.petshop.backend.entity.Supplier; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.SupplierRepository; +import com.petshop.backend.util.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -20,16 +21,16 @@ public class SupplierService { this.supplierRepository = supplierRepository; } + @Transactional(readOnly = true) public Page getAllSuppliers(String query, Pageable pageable) { - Page suppliers; - if (query != null && !query.trim().isEmpty()) { - suppliers = supplierRepository.searchSuppliers(query, pageable); - } else { - suppliers = supplierRepository.findAll(pageable); - } + String q = StringUtils.trimToNull(query); + Page suppliers = q != null + ? supplierRepository.searchSuppliers(q, pageable) + : supplierRepository.findAll(pageable); return suppliers.map(this::mapToResponse); } + @Transactional(readOnly = true) public SupplierResponse getSupplierById(Long id) { Supplier supplier = supplierRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Supplier not found with id: " + id)); @@ -39,12 +40,7 @@ public class SupplierService { @Transactional public SupplierResponse createSupplier(SupplierRequest request) { Supplier supplier = new Supplier(); - supplier.setSupCompany(request.getSupCompany()); - supplier.setSupContactFirstName(request.getSupContactFirstName()); - supplier.setSupContactLastName(request.getSupContactLastName()); - supplier.setSupEmail(request.getSupEmail()); - supplier.setSupPhone(request.getSupPhone()); - + applyFields(supplier, request); supplier = supplierRepository.save(supplier); return mapToResponse(supplier); } @@ -53,13 +49,7 @@ public class SupplierService { public SupplierResponse updateSupplier(Long id, SupplierRequest request) { Supplier supplier = supplierRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Supplier not found with id: " + id)); - - supplier.setSupCompany(request.getSupCompany()); - supplier.setSupContactFirstName(request.getSupContactFirstName()); - supplier.setSupContactLastName(request.getSupContactLastName()); - supplier.setSupEmail(request.getSupEmail()); - supplier.setSupPhone(request.getSupPhone()); - + applyFields(supplier, request); supplier = supplierRepository.save(supplier); return mapToResponse(supplier); } @@ -77,6 +67,14 @@ public class SupplierService { supplierRepository.deleteAllById(request.getIds()); } + private void applyFields(Supplier supplier, SupplierRequest request) { + supplier.setSupCompany(request.getSupCompany()); + supplier.setSupContactFirstName(request.getSupContactFirstName()); + supplier.setSupContactLastName(request.getSupContactLastName()); + supplier.setSupEmail(request.getSupEmail()); + supplier.setSupPhone(request.getSupPhone()); + } + private SupplierResponse mapToResponse(Supplier supplier) { return new SupplierResponse( supplier.getSupId(), From 4d96d1961c73d28d18d5abdd7ab8e3c91b3ceec0 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 17:08:00 -0600 Subject: [PATCH 10/14] inject AuthenticationHelper bean --- .../controller/AdoptionController.java | 19 ++++------ .../backend/controller/AiChatController.java | 37 +++++-------------- .../controller/AnalyticsController.java | 10 ++--- .../controller/AppointmentController.java | 19 ++++------ .../backend/controller/AuthController.java | 21 ++++------- .../backend/controller/ChatController.java | 36 +++++++----------- .../backend/controller/RefundController.java | 13 +++---- 7 files changed, 54 insertions(+), 101 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java index 84005778..1be65273 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -4,8 +4,6 @@ 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; import com.petshop.backend.service.AdoptionService; import com.petshop.backend.util.AuthenticationHelper; import jakarta.validation.Valid; @@ -23,11 +21,11 @@ import java.time.LocalDate; public class AdoptionController { private final AdoptionService adoptionService; - private final UserRepository userRepository; + private final AuthenticationHelper authHelper; - public AdoptionController(AdoptionService adoptionService, UserRepository userRepository) { + public AdoptionController(AdoptionService adoptionService, AuthenticationHelper authHelper) { this.adoptionService = adoptionService; - this.userRepository = userRepository; + this.authHelper = authHelper; } @GetMapping @@ -40,9 +38,7 @@ public class AdoptionController { @RequestParam(required = false) String date, Pageable pageable) { - Long effectiveCustomerId = AuthenticationHelper.isCustomer() - ? AuthenticationHelper.getAuthenticatedUser(userRepository).getId() - : customerId; + Long effectiveCustomerId = authHelper.getEffectiveCustomerId(customerId); LocalDate adoptionDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null; @@ -52,7 +48,7 @@ public class AdoptionController { @GetMapping("/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAdoptionById(@PathVariable Long id) { - Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); + Long customerId = authHelper.getCustomerIdOrNull(); return ResponseEntity.ok(adoptionService.getAdoptionById(id, customerId)); } @@ -65,16 +61,15 @@ public class AdoptionController { @PostMapping("/request") @PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')") public ResponseEntity 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()) + adoptionService.requestAdoption(authHelper.getAuthenticatedUser().getId(), request.getPetId(), request.getEmployeeId(), request.getSourceStoreId(), request.getAdoptionDate()) ); } @PatchMapping("/{id}/cancel") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity cancelAdoption(@PathVariable Long id) { - Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); + Long customerId = authHelper.getCustomerIdOrNull(); return ResponseEntity.ok(adoptionService.cancelAdoption(id, customerId)); } diff --git a/backend/src/main/java/com/petshop/backend/controller/AiChatController.java b/backend/src/main/java/com/petshop/backend/controller/AiChatController.java index f65dbaf0..6ed746a7 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AiChatController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AiChatController.java @@ -4,15 +4,14 @@ import com.petshop.backend.dto.ai.AiChatRequest; import com.petshop.backend.dto.ai.AiChatResponse; import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.User; +import com.petshop.backend.exception.BusinessException; import com.petshop.backend.repository.PetRepository; -import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.OpenRouterService; import com.petshop.backend.util.AuthenticationHelper; import com.petshop.backend.util.ContentFilter; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.bind.annotation.*; import java.util.Collections; @@ -24,43 +23,31 @@ public class AiChatController { private final OpenRouterService openRouterService; private final PetRepository petRepository; - private final UserRepository userRepository; + private final AuthenticationHelper authHelper; public AiChatController(OpenRouterService openRouterService, PetRepository petRepository, - UserRepository userRepository) { + AuthenticationHelper authHelper) { this.openRouterService = openRouterService; this.petRepository = petRepository; - this.userRepository = userRepository; - } - - private User getCurrentUser() { - try { - return AuthenticationHelper.getAuthenticatedUser(userRepository); - } - - catch (RuntimeException ex) { - throw new UsernameNotFoundException(ex.getMessage(), ex); - } + this.authHelper = authHelper; } @PostMapping("/message") @PreAuthorize("isAuthenticated()") public ResponseEntity sendMessage(@Valid @RequestBody AiChatRequest request) { if (request.getMessage() == null || request.getMessage().isBlank()) { - return ResponseEntity.badRequest().body(AiChatResponse.fail("Message cannot be empty")); + throw new BusinessException("Message cannot be empty"); } ContentFilter.validate(request.getMessage()); - User user = getCurrentUser(); + User user = authHelper.getAuthenticatedUser(); List userPets; try { userPets = petRepository.findAllByOwner_IdAndPetStatusInOrderByPetNameAsc( user.getId(), List.of("Adopted", "Owned")); - } - - catch (Exception e) { + } catch (Exception e) { userPets = Collections.emptyList(); } @@ -72,15 +59,9 @@ public class AiChatController { ); return ResponseEntity.ok(AiChatResponse.ok(aiReply)); - } - - catch (IllegalStateException e) { - + } catch (IllegalStateException e) { return ResponseEntity.status(503).body(AiChatResponse.fail("AI service is not configured. Please contact support.")); - } - - catch (Exception e) { - + } catch (Exception e) { return ResponseEntity.status(502).body(AiChatResponse.fail("AI service is temporarily unavailable. Please try again later.")); } } diff --git a/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java b/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java index fedf5a16..08aea44d 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AnalyticsController.java @@ -1,8 +1,6 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.analytics.DashboardResponse; -import com.petshop.backend.entity.User; -import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AnalyticsService; import com.petshop.backend.util.AuthenticationHelper; import org.springframework.format.annotation.DateTimeFormat; @@ -20,11 +18,11 @@ import java.time.LocalDate; public class AnalyticsController { private final AnalyticsService analyticsService; - private final UserRepository userRepository; + private final AuthenticationHelper authHelper; - public AnalyticsController(AnalyticsService analyticsService, UserRepository userRepository) { + public AnalyticsController(AnalyticsService analyticsService, AuthenticationHelper authHelper) { this.analyticsService = analyticsService; - this.userRepository = userRepository; + this.authHelper = authHelper; } @GetMapping("/dashboard") @@ -41,7 +39,7 @@ public class AnalyticsController { if (top < 1 || top > 50) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "top must be between 1 and 50"); } - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); + var user = authHelper.getAuthenticatedUser(); java.time.LocalDateTime endDateTime = endDate != null ? endDate.plusDays(1).atStartOfDay() : null; return ResponseEntity.ok(analyticsService.getDashboardData(days, top, user, paymentMethod, storeId, channel, endDateTime)); } diff --git a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java index 364f836e..301b4a77 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -3,8 +3,6 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.appointment.AppointmentRequest; import com.petshop.backend.dto.appointment.AppointmentResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; -import com.petshop.backend.entity.User; -import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.AppointmentService; import com.petshop.backend.util.AuthenticationHelper; import jakarta.validation.Valid; @@ -23,11 +21,11 @@ import java.util.List; public class AppointmentController { private final AppointmentService appointmentService; - private final UserRepository userRepository; + private final AuthenticationHelper authHelper; - public AppointmentController(AppointmentService appointmentService, UserRepository userRepository) { + public AppointmentController(AppointmentService appointmentService, AuthenticationHelper authHelper) { this.appointmentService = appointmentService; - this.userRepository = userRepository; + this.authHelper = authHelper; } @GetMapping @@ -41,9 +39,7 @@ public class AppointmentController { @RequestParam(required = false) Long employeeId, Pageable pageable) { - Long effectiveCustomerId = AuthenticationHelper.isCustomer() - ? AuthenticationHelper.getAuthenticatedUser(userRepository).getId() - : customerId; + Long effectiveCustomerId = authHelper.getEffectiveCustomerId(customerId); LocalDate appointmentDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null; @@ -54,7 +50,7 @@ public class AppointmentController { @GetMapping("/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAppointmentById(@PathVariable Long id) { - Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); + Long customerId = authHelper.getCustomerIdOrNull(); return ResponseEntity.ok(appointmentService.getAppointmentById(id, customerId)); } @@ -62,8 +58,7 @@ public class AppointmentController { @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity createAppointment(@Valid @RequestBody AppointmentRequest request) { if (AuthenticationHelper.isCustomer()) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - if (!request.getCustomerId().equals(user.getId())) { + if (!request.getCustomerId().equals(authHelper.getAuthenticatedUser().getId())) { throw new org.springframework.security.access.AccessDeniedException("You can only create appointments for yourself"); } } @@ -74,7 +69,7 @@ public class AppointmentController { @PatchMapping("/{id}/cancel") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity cancelAppointment(@PathVariable Long id) { - Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); + Long customerId = authHelper.getCustomerIdOrNull(); return ResponseEntity.ok(appointmentService.cancelAppointment(id, customerId)); } diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index b0b1ffa5..b366bd02 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -64,8 +64,9 @@ public class AuthController { private final PasswordResetService passwordResetService; private final EmailService emailService; private final UserAuthCacheService userAuthCacheService; + private final AuthenticationHelper authHelper; - public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService, PasswordResetService passwordResetService, EmailService emailService, UserAuthCacheService userAuthCacheService) { + public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService, PasswordResetService passwordResetService, EmailService emailService, UserAuthCacheService userAuthCacheService, AuthenticationHelper authHelper) { this.authenticationManager = authenticationManager; this.userRepository = userRepository; this.jwtUtil = jwtUtil; @@ -75,6 +76,7 @@ public class AuthController { this.passwordResetService = passwordResetService; this.emailService = emailService; this.userAuthCacheService = userAuthCacheService; + this.authHelper = authHelper; } @PostMapping("/register") @@ -180,7 +182,7 @@ public class AuthController { @Transactional(readOnly = true) @GetMapping("/me") public ResponseEntity getCurrentUser() { - User user = getAuthenticatedUser(); + User user = authHelper.getAuthenticatedUser(); return ResponseEntity.ok(toUserInfoResponse(user)); } @@ -278,7 +280,7 @@ public class AuthController { @PostMapping("/me/avatar") public ResponseEntity uploadAvatar(@RequestParam("avatar") MultipartFile file) { - User user = getAuthenticatedUser(); + User user = authHelper.getAuthenticatedUser(); ImageValidationUtil.validate(file); @@ -297,7 +299,7 @@ public class AuthController { @GetMapping("/me/avatar") public ResponseEntity getAvatar() { - User user = getAuthenticatedUser(); + User user = authHelper.getAuthenticatedUser(); if (!avatarStorageService.hasAvatar(user)) { throw new ResourceNotFoundException("No avatar uploaded"); @@ -310,7 +312,7 @@ public class AuthController { @GetMapping("/me/avatar/file") public ResponseEntity getAvatarFile() { - User user = getAuthenticatedUser(); + User user = authHelper.getAuthenticatedUser(); if (!avatarStorageService.hasAvatar(user)) { return ResponseEntity.notFound().build(); @@ -327,7 +329,7 @@ public class AuthController { @DeleteMapping("/me/avatar") public ResponseEntity deleteAvatar() { - User user = getAuthenticatedUser(); + User user = authHelper.getAuthenticatedUser(); if (avatarStorageService.hasAvatar(user)) { try { @@ -352,11 +354,4 @@ public class AuthController { return ResponseEntity.ok(response); } - private User getAuthenticatedUser() { - try { - return AuthenticationHelper.getAuthenticatedUser(userRepository); - } catch (RuntimeException ex) { - throw new UsernameNotFoundException(ex.getMessage(), ex); - } - } } diff --git a/backend/src/main/java/com/petshop/backend/controller/ChatController.java b/backend/src/main/java/com/petshop/backend/controller/ChatController.java index 94fb229a..f1e26209 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ChatController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ChatController.java @@ -9,7 +9,6 @@ import com.petshop.backend.entity.Message; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.MessageRepository; -import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.ChatAttachmentStorageService; import com.petshop.backend.service.ChatRealtimeService; import com.petshop.backend.service.ChatService; @@ -22,7 +21,6 @@ import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -35,33 +33,25 @@ public class ChatController { private final ChatService chatService; private final ChatRealtimeService chatRealtimeService; private final OpenRouterAiService openRouterAiService; - private final UserRepository userRepository; + private final AuthenticationHelper authHelper; private final ChatAttachmentStorageService attachmentStorageService; private final MessageRepository messageRepository; - public ChatController(ChatService chatService, ChatRealtimeService chatRealtimeService, - OpenRouterAiService openRouterAiService, UserRepository userRepository, ChatAttachmentStorageService attachmentStorageService, + public ChatController(ChatService chatService, ChatRealtimeService chatRealtimeService, + OpenRouterAiService openRouterAiService, AuthenticationHelper authHelper, ChatAttachmentStorageService attachmentStorageService, MessageRepository messageRepository) { this.chatService = chatService; this.chatRealtimeService = chatRealtimeService; this.openRouterAiService = openRouterAiService; - this.userRepository = userRepository; + this.authHelper = authHelper; this.attachmentStorageService = attachmentStorageService; this.messageRepository = messageRepository; } - private User getCurrentUser() { - try { - return AuthenticationHelper.getAuthenticatedUser(userRepository); - } catch (RuntimeException ex) { - throw new UsernameNotFoundException(ex.getMessage(), ex); - } - } - @PostMapping("/conversations") @PreAuthorize("isAuthenticated()") public ResponseEntity createConversation(@Valid @RequestBody ConversationRequest request) { - User user = getCurrentUser(); + User user = authHelper.getAuthenticatedUser(); ConversationResponse response = chatService.createConversation(user.getId(), request); chatRealtimeService.publishNewConversation(response); return ResponseEntity.status(HttpStatus.CREATED).body(response); @@ -71,7 +61,7 @@ public class ChatController { @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity> getConversations( @RequestParam(required = false, defaultValue = "false") boolean mine) { - User user = getCurrentUser(); + User user = authHelper.getAuthenticatedUser(); List conversations = chatService.getConversations(user.getId(), user.getRole(), mine); return ResponseEntity.ok(conversations); } @@ -79,7 +69,7 @@ public class ChatController { @GetMapping("/conversations/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getConversation(@PathVariable Long id) { - User user = getCurrentUser(); + User user = authHelper.getAuthenticatedUser(); ConversationResponse conversation = chatService.getConversation(id, user.getId(), user.getRole()); return ResponseEntity.ok(conversation); } @@ -89,7 +79,7 @@ public class ChatController { public ResponseEntity sendMessage( @PathVariable Long id, @Valid @RequestBody MessageRequest request) { - User user = getCurrentUser(); + User user = authHelper.getAuthenticatedUser(); MessageResponse message = chatService.sendMessage(id, user.getId(), user.getRole(), request); chatRealtimeService.publishMessage(id, message); chatRealtimeService.publishConversationUpdate(id); @@ -103,7 +93,7 @@ public class ChatController { @PathVariable Long id, @RequestParam("file") MultipartFile file, @RequestParam(value = "content", required = false) String content) { - User user = getCurrentUser(); + User user = authHelper.getAuthenticatedUser(); MessageResponse message = chatService.sendMessageWithAttachment(id, user.getId(), user.getRole(), file, content); chatRealtimeService.publishMessage(id, message); chatRealtimeService.publishConversationUpdate(id); @@ -114,7 +104,7 @@ public class ChatController { @GetMapping("/messages/{messageId}/attachment") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getMessageAttachment(@PathVariable Long messageId) { - User user = getCurrentUser(); + User user = authHelper.getAuthenticatedUser(); Message message = messageRepository.findById(messageId) .orElseThrow(() -> new ResourceNotFoundException("Message not found with id: " + messageId)); @@ -140,7 +130,7 @@ public class ChatController { @GetMapping("/conversations/{id}/messages") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity> getMessages(@PathVariable Long id) { - User user = getCurrentUser(); + User user = authHelper.getAuthenticatedUser(); List messages = chatService.getMessages(id, user.getId(), user.getRole()); return ResponseEntity.ok(messages); } @@ -148,7 +138,7 @@ public class ChatController { @PostMapping("/conversations/{id}/request-human") @PreAuthorize("isAuthenticated()") public ResponseEntity requestHumanTakeover(@PathVariable Long id) { - User user = getCurrentUser(); + User user = authHelper.getAuthenticatedUser(); ConversationResponse conversation = chatService.requestHumanTakeover(id, user.getId(), user.getRole()); chatRealtimeService.publishConversationUpdate(id); return ResponseEntity.ok(conversation); @@ -157,7 +147,7 @@ public class ChatController { @PutMapping("/conversations/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity updateConversation(@PathVariable Long id, @Valid @RequestBody UpdateConversationRequest request) { - User user = getCurrentUser(); + User user = authHelper.getAuthenticatedUser(); ConversationResponse conversation = chatService.updateConversation(id, user.getId(), user.getRole(), request); chatRealtimeService.publishConversationUpdate(id); return ResponseEntity.ok(conversation); diff --git a/backend/src/main/java/com/petshop/backend/controller/RefundController.java b/backend/src/main/java/com/petshop/backend/controller/RefundController.java index da58c6a0..b8e20241 100644 --- a/backend/src/main/java/com/petshop/backend/controller/RefundController.java +++ b/backend/src/main/java/com/petshop/backend/controller/RefundController.java @@ -3,7 +3,6 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.refund.RefundRequest; import com.petshop.backend.dto.refund.RefundResponse; import com.petshop.backend.dto.refund.RefundUpdateRequest; -import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.RefundService; import com.petshop.backend.util.AuthenticationHelper; import jakarta.validation.Valid; @@ -19,24 +18,24 @@ import java.util.List; public class RefundController { private final RefundService refundService; - private final UserRepository userRepository; + private final AuthenticationHelper authHelper; - public RefundController(RefundService refundService, UserRepository userRepository) { + public RefundController(RefundService refundService, AuthenticationHelper authHelper) { this.refundService = refundService; - this.userRepository = userRepository; + this.authHelper = authHelper; } @PostMapping @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF')") public ResponseEntity createRefund(@Valid @RequestBody RefundRequest request) { - Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); + Long customerId = authHelper.getCustomerIdOrNull(); return ResponseEntity.status(HttpStatus.CREATED).body(refundService.createRefund(request, customerId)); } @GetMapping @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity> getAllRefunds() { - Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); + Long customerId = authHelper.getCustomerIdOrNull(); List refunds = refundService.getAllRefunds(customerId); return ResponseEntity.ok(refunds); } @@ -44,7 +43,7 @@ public class RefundController { @GetMapping("/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getRefundById(@PathVariable Long id) { - Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); + Long customerId = authHelper.getCustomerIdOrNull(); return ResponseEntity.ok(refundService.getRefundById(id, customerId)); } From 1ce390e528d0b0cac1a93a45a7613b4679e45721 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 17:35:00 -0600 Subject: [PATCH 11/14] simplify controllers and utilities --- .../backend/controller/MyPetController.java | 30 +++++-------------- .../backend/util/AuthenticationHelper.java | 18 +++++++++-- .../com/petshop/backend/util/StringUtils.java | 16 ++++++++++ 3 files changed, 39 insertions(+), 25 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/MyPetController.java b/backend/src/main/java/com/petshop/backend/controller/MyPetController.java index 18e8e914..5d4ad1ef 100644 --- a/backend/src/main/java/com/petshop/backend/controller/MyPetController.java +++ b/backend/src/main/java/com/petshop/backend/controller/MyPetController.java @@ -2,8 +2,6 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.pet.MyPetRequest; import com.petshop.backend.dto.pet.MyPetResponse; -import com.petshop.backend.entity.User; -import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.PetService; import com.petshop.backend.util.AuthenticationHelper; import jakarta.validation.Valid; @@ -22,7 +20,6 @@ import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.util.List; -import java.util.Map; @RestController @RequestMapping("/api/v1/my-pets") @@ -30,47 +27,34 @@ import java.util.Map; public class MyPetController { private final PetService petService; - private final UserRepository userRepository; - public MyPetController(PetService petService, UserRepository userRepository) { + public MyPetController(PetService petService) { this.petService = petService; - this.userRepository = userRepository; } @GetMapping public ResponseEntity> getMyPets(@RequestParam(required = false) String status) { - return ResponseEntity.ok(petService.getMyPets(currentUserId(), status)); + return ResponseEntity.ok(petService.getMyPets(AuthenticationHelper.getAuthenticatedUserId(), status)); } @PostMapping public ResponseEntity createMyPet(@Valid @RequestBody MyPetRequest request) { - return ResponseEntity.ok(petService.createMyPet(currentUserId(), request)); + return ResponseEntity.ok(petService.createMyPet(AuthenticationHelper.getAuthenticatedUserId(), request)); } @PutMapping("/{id}") public ResponseEntity updateMyPet(@PathVariable Long id, @Valid @RequestBody MyPetRequest request) { - return ResponseEntity.ok(petService.updateMyPet(currentUserId(), id, request)); + return ResponseEntity.ok(petService.updateMyPet(AuthenticationHelper.getAuthenticatedUserId(), id, request)); } @DeleteMapping("/{id}") public ResponseEntity deleteMyPet(@PathVariable Long id) { - petService.deleteMyPet(currentUserId(), id); + petService.deleteMyPet(AuthenticationHelper.getAuthenticatedUserId(), id); return ResponseEntity.noContent().build(); } @PostMapping("/{id}/image") - public ResponseEntity uploadMyPetImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) { - try { - return ResponseEntity.ok(petService.uploadMyPetImage(currentUserId(), id, image)); - } catch (IllegalArgumentException ex) { - return ResponseEntity.badRequest().body(Map.of("message", ex.getMessage())); - } catch (IOException ex) { - return ResponseEntity.badRequest().body(Map.of("message", "Failed to upload pet image: " + ex.getMessage())); - } - } - - private Long currentUserId() { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - return user.getId(); + public ResponseEntity uploadMyPetImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) throws IOException { + return ResponseEntity.ok(petService.uploadMyPetImage(AuthenticationHelper.getAuthenticatedUserId(), id, image)); } } diff --git a/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java index 4586ab2d..71f041ab 100644 --- a/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java +++ b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java @@ -13,6 +13,12 @@ import org.springframework.stereotype.Component; @Component public class AuthenticationHelper { + private final UserRepository userRepository; + + public AuthenticationHelper(UserRepository userRepository) { + this.userRepository = userRepository; + } + public static Authentication getAuthentication() { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) { @@ -41,11 +47,19 @@ public class AuthenticationHelper { return getAuthenticatedPrincipal().getRole() == User.Role.CUSTOMER; } - public static Long getCustomerIdOrNull(UserRepository userRepository) { + public User getAuthenticatedUser() { + return getAuthenticatedUser(userRepository); + } + + public Long getCustomerIdOrNull() { if (!isCustomer()) { return null; } - return getAuthenticatedUser(userRepository).getId(); + return getAuthenticatedUser().getId(); + } + + public Long getEffectiveCustomerId(Long requestedCustomerId) { + return isCustomer() ? getAuthenticatedUser().getId() : requestedCustomerId; } public static User getAuthenticatedUser(UserRepository userRepository) { diff --git a/backend/src/main/java/com/petshop/backend/util/StringUtils.java b/backend/src/main/java/com/petshop/backend/util/StringUtils.java index 625c294d..fd795822 100644 --- a/backend/src/main/java/com/petshop/backend/util/StringUtils.java +++ b/backend/src/main/java/com/petshop/backend/util/StringUtils.java @@ -11,4 +11,20 @@ public final class StringUtils { String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } + + public static String fullName(String firstName, String lastName) { + String first = trimToNull(firstName); + String last = trimToNull(lastName); + if (first == null) { + return last; + } + if (last == null) { + return first; + } + return first + " " + last; + } + + public static String normalizePhone(String value) { + return trimToNull(PhoneUtils.normalize(trimToNull(value))); + } } From 99768ec9b94ceb6b3745ded32e0468095735445e Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 17:58:00 -0600 Subject: [PATCH 12/14] add read-only transactional annotations --- .../backend/service/AdoptionService.java | 2 ++ .../backend/service/CouponService.java | 3 +++ .../backend/service/InventoryService.java | 2 ++ .../backend/service/ProductService.java | 26 +++++-------------- .../service/ProductSupplierService.java | 2 ++ .../backend/service/PurchaseOrderService.java | 3 +++ .../petshop/backend/service/UserService.java | 2 ++ 7 files changed, 21 insertions(+), 19 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 6a6c32e9..b99be2e2 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -56,6 +56,7 @@ public class AdoptionService { this.eventPublisher = eventPublisher; } + @Transactional(readOnly = true) public Page getAllAdoptions(String query, Long customerId, String status, Long storeId, LocalDate date, Pageable pageable) { String normalizedQuery = StringUtils.trimToNull(query); String normalizedStatus = StringUtils.trimToNull(status); @@ -72,6 +73,7 @@ public class AdoptionService { return adoptions.map(this::mapToResponse); } + @Transactional(readOnly = true) public AdoptionResponse getAdoptionById(Long id, Long customerId) { Adoption adoption = adoptionRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + id)); diff --git a/backend/src/main/java/com/petshop/backend/service/CouponService.java b/backend/src/main/java/com/petshop/backend/service/CouponService.java index 4cb9f295..173be890 100644 --- a/backend/src/main/java/com/petshop/backend/service/CouponService.java +++ b/backend/src/main/java/com/petshop/backend/service/CouponService.java @@ -28,16 +28,19 @@ public class CouponService { this.saleRepository = saleRepository; } + @Transactional(readOnly = true) public Page getAllCoupons(String query, Boolean active, Pageable pageable) { return couponRepository.searchCoupons(query, active, pageable).map(this::mapToResponse); } + @Transactional(readOnly = true) public CouponResponse getCouponById(Long id) { Coupon coupon = couponRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Coupon not found with id: " + id)); return mapToResponse(coupon); } + @Transactional(readOnly = true) public CouponResponse getCouponByCode(String code) { Coupon coupon = couponRepository.findByCouponCode(code) .orElseThrow(() -> new ResourceNotFoundException("Coupon not found with code: " + code)); diff --git a/backend/src/main/java/com/petshop/backend/service/InventoryService.java b/backend/src/main/java/com/petshop/backend/service/InventoryService.java index 021ed021..af133e68 100644 --- a/backend/src/main/java/com/petshop/backend/service/InventoryService.java +++ b/backend/src/main/java/com/petshop/backend/service/InventoryService.java @@ -29,12 +29,14 @@ public class InventoryService { this.storeRepository = storeRepository; } + @Transactional(readOnly = true) public Page getAllInventory(String query, Long storeId, Pageable pageable) { String normalizedQuery = StringUtils.trimToNull(query); Page inventory = inventoryRepository.searchInventory(normalizedQuery, storeId, pageable); return inventory.map(this::mapToResponse); } + @Transactional(readOnly = true) public InventoryResponse getInventoryById(Long id) { Inventory inventory = inventoryRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Inventory not found with id: " + id)); diff --git a/backend/src/main/java/com/petshop/backend/service/ProductService.java b/backend/src/main/java/com/petshop/backend/service/ProductService.java index fc292f36..ad4524fe 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductService.java @@ -11,8 +11,6 @@ import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.CategoryRepository; import com.petshop.backend.repository.ProductRepository; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.data.domain.Page; @@ -27,8 +25,6 @@ import java.io.IOException; @Service public class ProductService { - private static final Logger log = LoggerFactory.getLogger(ProductService.class); - private final ProductRepository productRepository; private final CategoryRepository categoryRepository; private final CatalogImageStorageService catalogImageStorageService; @@ -39,11 +35,13 @@ public class ProductService { this.catalogImageStorageService = catalogImageStorageService; } + @Transactional(readOnly = true) public Page getAllProducts(String query, Long categoryId, Pageable pageable) { return productRepository.searchProducts(StringUtils.trimToNull(query), categoryId, pageable) .map(this::mapToResponse); } + @Transactional(readOnly = true) public ProductResponse getProductById(Long id) { Product product = productRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + id)); @@ -93,13 +91,13 @@ public class ProductService { @Transactional public void deleteProduct(Long id) { Product product = findProduct(id); - deleteStoredImageIfPresent(product.getImageUrl()); + catalogImageStorageService.deleteProductImageIfPresent(product.getImageUrl()); productRepository.delete(product); } @Transactional public void bulkDeleteProducts(BulkDeleteRequest request) { - productRepository.findAllById(request.getIds()).forEach(product -> deleteStoredImageIfPresent(product.getImageUrl())); + productRepository.findAllById(request.getIds()).forEach(product -> catalogImageStorageService.deleteProductImageIfPresent(product.getImageUrl())); productRepository.deleteAllById(request.getIds()); } @@ -107,7 +105,7 @@ public class ProductService { public ProductResponse uploadProductImage(Long id, MultipartFile file) throws IOException { validateImageFile(file); Product product = findProduct(id); - deleteStoredImageIfPresent(product.getImageUrl()); + catalogImageStorageService.deleteProductImageIfPresent(product.getImageUrl()); product.setImageUrl(catalogImageStorageService.storeProductImage(file)); return mapToResponse(productRepository.save(product)); } @@ -115,11 +113,12 @@ public class ProductService { @Transactional public ProductResponse deleteProductImage(Long id) { Product product = findProduct(id); - deleteStoredImageIfPresent(product.getImageUrl()); + catalogImageStorageService.deleteProductImageIfPresent(product.getImageUrl()); product.setImageUrl(null); return mapToResponse(productRepository.save(product)); } + @Transactional(readOnly = true) public ImagePayload loadProductImage(Long id) { Product product = findProduct(id); if (product.getImageUrl() == null || product.getImageUrl().isBlank()) { @@ -142,17 +141,6 @@ public class ProductService { ImageValidationUtil.validate(file); } - private void deleteStoredImageIfPresent(String storedImagePath) { - if (storedImagePath == null || storedImagePath.isBlank()) { - return; - } - try { - catalogImageStorageService.deleteProductImage(storedImagePath); - } catch (IOException | IllegalArgumentException e) { - log.warn("Failed to delete stored image {}: {}", storedImagePath, e.getMessage()); - } - } - private ProductResponse mapToResponse(Product product) { return new ProductResponse( product.getProdId(), diff --git a/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java index 0f6250d2..5adc79fb 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java @@ -34,6 +34,7 @@ public class ProductSupplierService { this.supplierRepository = supplierRepository; } + @Transactional(readOnly = true) public Page getAllProductSuppliers(String query, Long productId, Long supplierId, Pageable pageable) { String normalizedQuery = StringUtils.trimToNull(query); Pageable mappedPageable = mapSortProperties(pageable); @@ -41,6 +42,7 @@ public class ProductSupplierService { return productSuppliers.map(this::mapToResponse); } + @Transactional(readOnly = true) public ProductSupplierResponse getProductSupplierById(Long productId, Long supplierId) { ProductSupplier.ProductSupplierId id = new ProductSupplier.ProductSupplierId(productId, supplierId); ProductSupplier productSupplier = productSupplierRepository.findById(id) diff --git a/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java index 0f2a1d07..a6803d37 100644 --- a/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java +++ b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java @@ -9,6 +9,7 @@ import com.petshop.backend.repository.PurchaseOrderRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service public class PurchaseOrderService { @@ -19,12 +20,14 @@ public class PurchaseOrderService { this.purchaseOrderRepository = purchaseOrderRepository; } + @Transactional(readOnly = true) public Page getAllPurchaseOrders(String query, Long storeId, Pageable pageable) { String normalizedQuery = StringUtils.trimToNull(query); Page purchaseOrders = purchaseOrderRepository.searchPurchaseOrders(normalizedQuery, storeId, pageable); return purchaseOrders.map(this::mapToResponse); } + @Transactional(readOnly = true) public PurchaseOrderResponse getPurchaseOrderById(Long id) { PurchaseOrder purchaseOrder = purchaseOrderRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("PurchaseOrder not found with id: " + id)); diff --git a/backend/src/main/java/com/petshop/backend/service/UserService.java b/backend/src/main/java/com/petshop/backend/service/UserService.java index d5cd1243..99e25747 100644 --- a/backend/src/main/java/com/petshop/backend/service/UserService.java +++ b/backend/src/main/java/com/petshop/backend/service/UserService.java @@ -44,6 +44,7 @@ public class UserService { this.userAuthCacheService = userAuthCacheService; } + @Transactional(readOnly = true) public Page getAllUsers(String query, String role, Pageable pageable) { User.Role parsedRole = parseRole(role); Page users; @@ -60,6 +61,7 @@ public class UserService { return users.map(this::mapToResponse); } + @Transactional(readOnly = true) public UserResponse getUserById(Long id) { return getUserById(id, null); } From e2b9ae6e0cf4febf6ab32b605a383ffb610f06f9 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 18:24:00 -0600 Subject: [PATCH 13/14] extract image delete to storage --- .../service/CatalogImageStorageService.java | 22 +++++++++++++++++++ .../petshop/backend/service/PetService.java | 22 +++++-------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/service/CatalogImageStorageService.java b/backend/src/main/java/com/petshop/backend/service/CatalogImageStorageService.java index 48565753..229ac5ba 100644 --- a/backend/src/main/java/com/petshop/backend/service/CatalogImageStorageService.java +++ b/backend/src/main/java/com/petshop/backend/service/CatalogImageStorageService.java @@ -1,5 +1,7 @@ package com.petshop.backend.service; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.ByteArrayResource; @@ -21,6 +23,8 @@ import java.util.UUID; @Service public class CatalogImageStorageService { + private static final Logger log = LoggerFactory.getLogger(CatalogImageStorageService.class); + private static final String PET_PREFIX = "/uploads/pets/"; private static final String PRODUCT_PREFIX = "/uploads/products/"; private static final String BLOB_PETS = "pets"; @@ -106,6 +110,24 @@ public class CatalogImageStorageService { } } + public void deletePetImageIfPresent(String storedPath) { + if (storedPath == null || storedPath.isBlank()) return; + try { + deletePetImage(storedPath); + } catch (IOException | IllegalArgumentException e) { + log.warn("Failed to delete pet image {}: {}", storedPath, e.getMessage()); + } + } + + public void deleteProductImageIfPresent(String storedPath) { + if (storedPath == null || storedPath.isBlank()) return; + try { + deleteProductImage(storedPath); + } catch (IOException | IllegalArgumentException e) { + log.warn("Failed to delete product image {}: {}", storedPath, e.getMessage()); + } + } + private String extractFilename(String storedPath, String prefix) { if (storedPath == null || !storedPath.startsWith(prefix)) throw new IllegalArgumentException("Image file was not found"); String filename = storedPath.substring(prefix.length()); 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 e1aacbd2..1c4cf875 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -136,7 +136,7 @@ public class PetService { appt.setPet(null); } appointmentRepository.saveAll(linkedAppointments); - deleteStoredImageIfPresent(pet.getImageUrl()); + catalogImageStorageService.deletePetImageIfPresent(pet.getImageUrl()); petRepository.delete(pet); } @@ -144,7 +144,7 @@ public class PetService { public MyPetResponse uploadMyPetImage(Long ownerUserId, Long petId, MultipartFile file) throws IOException { validateImageFile(file); Pet pet = findOwnedPet(ownerUserId, petId); - deleteStoredImageIfPresent(pet.getImageUrl()); + catalogImageStorageService.deletePetImageIfPresent(pet.getImageUrl()); pet.setImageUrl(catalogImageStorageService.storePetImage(file)); return mapToMyPetResponse(petRepository.save(pet)); } @@ -185,13 +185,13 @@ public class PetService { public void deletePet(Long id) { Pet pet = petRepository.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id)); - deleteStoredImageIfPresent(pet.getImageUrl()); + catalogImageStorageService.deletePetImageIfPresent(pet.getImageUrl()); petRepository.delete(pet); } @Transactional public void bulkDeletePets(BulkDeleteRequest request) { - petRepository.findAllById(request.getIds()).forEach(pet -> deleteStoredImageIfPresent(pet.getImageUrl())); + petRepository.findAllById(request.getIds()).forEach(pet -> catalogImageStorageService.deletePetImageIfPresent(pet.getImageUrl())); petRepository.deleteAllById(request.getIds()); } @@ -199,7 +199,7 @@ public class PetService { public PetResponse uploadPetImage(Long id, MultipartFile file) throws IOException { validateImageFile(file); Pet pet = findPet(id); - deleteStoredImageIfPresent(pet.getImageUrl()); + catalogImageStorageService.deletePetImageIfPresent(pet.getImageUrl()); pet.setImageUrl(catalogImageStorageService.storePetImage(file)); return mapToResponse(petRepository.save(pet)); } @@ -207,7 +207,7 @@ public class PetService { @Transactional public PetResponse deletePetImage(Long id) { Pet pet = findPet(id); - deleteStoredImageIfPresent(pet.getImageUrl()); + catalogImageStorageService.deletePetImageIfPresent(pet.getImageUrl()); pet.setImageUrl(null); return mapToResponse(petRepository.save(pet)); } @@ -293,16 +293,6 @@ public class PetService { .orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + petId)); } - private void deleteStoredImageIfPresent(String storedImagePath) { - if (storedImagePath == null || storedImagePath.isBlank()) { - return; - } - try { - catalogImageStorageService.deletePetImage(storedImagePath); - } catch (IOException | IllegalArgumentException ignored) { - } - } - private String normalizeStatus(String status) { return status == null ? "" : status.trim(); } From f5430a1940e1bbf4f96834ececed98c07110b78a Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Fri, 17 Apr 2026 18:52:00 -0600 Subject: [PATCH 14/14] fix compatibility regressions --- .../backend/controller/AiChatController.java | 3 +-- .../backend/controller/AuthController.java | 17 ++++++----------- .../exception/GlobalExceptionHandler.java | 6 ++++++ .../petshop/backend/service/CouponService.java | 8 ++++++-- 4 files changed, 19 insertions(+), 15 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/controller/AiChatController.java b/backend/src/main/java/com/petshop/backend/controller/AiChatController.java index 6ed746a7..a20ef4f7 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AiChatController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AiChatController.java @@ -4,7 +4,6 @@ import com.petshop.backend.dto.ai.AiChatRequest; import com.petshop.backend.dto.ai.AiChatResponse; import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.User; -import com.petshop.backend.exception.BusinessException; import com.petshop.backend.repository.PetRepository; import com.petshop.backend.service.OpenRouterService; import com.petshop.backend.util.AuthenticationHelper; @@ -37,7 +36,7 @@ public class AiChatController { @PreAuthorize("isAuthenticated()") public ResponseEntity sendMessage(@Valid @RequestBody AiChatRequest request) { if (request.getMessage() == null || request.getMessage().isBlank()) { - throw new BusinessException("Message cannot be empty"); + return ResponseEntity.badRequest().body(AiChatResponse.fail("Message cannot be empty")); } ContentFilter.validate(request.getMessage()); diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index b366bd02..a71c3777 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -279,22 +279,17 @@ public class AuthController { } @PostMapping("/me/avatar") - public ResponseEntity uploadAvatar(@RequestParam("avatar") MultipartFile file) { + public ResponseEntity uploadAvatar(@RequestParam("avatar") MultipartFile file) throws IOException { User user = authHelper.getAuthenticatedUser(); ImageValidationUtil.validate(file); - try { - avatarStorageService.deleteAvatar(user); - String avatarPath = avatarStorageService.storeAvatar(file); - user.setAvatarUrl(avatarPath); - userRepository.save(user); + avatarStorageService.deleteAvatar(user); + String avatarPath = avatarStorageService.storeAvatar(file); + user.setAvatarUrl(avatarPath); + userRepository.save(user); - return ResponseEntity.ok(new AvatarUploadResponse(avatarStorageService.toOwnerAvatarUrl(user), "Avatar uploaded successfully")); - - } catch (IOException e) { - throw new BusinessException("Failed to upload avatar: " + e.getMessage()); - } + return ResponseEntity.ok(new AvatarUploadResponse(avatarStorageService.toOwnerAvatarUrl(user), "Avatar uploaded successfully")); } @GetMapping("/me/avatar") diff --git a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java index 29b7f10d..3801b3ea 100644 --- a/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java +++ b/backend/src/main/java/com/petshop/backend/exception/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ package com.petshop.backend.exception; import com.petshop.backend.service.PetService; import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.core.PropertyReferenceException; import org.springframework.http.HttpStatus; @@ -121,6 +122,11 @@ public class GlobalExceptionHandler { return buildErrorResponse(HttpStatus.FORBIDDEN, "Access to this pet image is not allowed", ex, request); } + @ExceptionHandler(IOException.class) + public ResponseEntity handleIOException(IOException ex, HttpServletRequest request) { + return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), ex, request); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleGenericException(Exception ex, HttpServletRequest request) { String message = ex.getMessage() == null || ex.getMessage().isBlank() diff --git a/backend/src/main/java/com/petshop/backend/service/CouponService.java b/backend/src/main/java/com/petshop/backend/service/CouponService.java index 173be890..9625cfed 100644 --- a/backend/src/main/java/com/petshop/backend/service/CouponService.java +++ b/backend/src/main/java/com/petshop/backend/service/CouponService.java @@ -139,11 +139,15 @@ public class CouponService { } public static boolean isPercentageType(String discountType) { - return "PERCENTAGE".equalsIgnoreCase(discountType) || "PERCENT".equalsIgnoreCase(discountType); + if (discountType == null) return false; + String type = discountType.trim(); + return "PERCENTAGE".equalsIgnoreCase(type) || "PERCENT".equalsIgnoreCase(type); } public static boolean isFixedType(String discountType) { - return "FIXED".equalsIgnoreCase(discountType) || "FLAT".equalsIgnoreCase(discountType); + if (discountType == null) return false; + String type = discountType.trim(); + return "FIXED".equalsIgnoreCase(type) || "FLAT".equalsIgnoreCase(type); } private CouponResponse mapToResponse(Coupon coupon) {