unify error handling

This commit is contained in:
2026-04-17 13:31:00 -06:00
parent 46bdd5c3d7
commit 4162e34a5f
9 changed files with 85 additions and 98 deletions

View File

@@ -13,6 +13,9 @@ import com.petshop.backend.dto.auth.ResetPasswordResponse;
import com.petshop.backend.dto.auth.UserInfoResponse; import com.petshop.backend.dto.auth.UserInfoResponse;
import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User; 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.repository.UserRepository;
import com.petshop.backend.security.JwtUtil; import com.petshop.backend.security.JwtUtil;
import com.petshop.backend.security.UserAuthCacheService; import com.petshop.backend.security.UserAuthCacheService;
@@ -74,7 +77,7 @@ public class AuthController {
} }
@PostMapping("/register") @PostMapping("/register")
public ResponseEntity<?> register(@Valid @RequestBody RegisterRequest request) { public ResponseEntity<RegisterResponse> register(@Valid @RequestBody RegisterRequest request) {
String username = trimToNull(request.getUsername()); String username = trimToNull(request.getUsername());
String email = trimToNull(request.getEmail()); String email = trimToNull(request.getEmail());
String firstName = trimToNull(request.getFirstName()); String firstName = trimToNull(request.getFirstName());
@@ -83,23 +86,17 @@ public class AuthController {
if (userRepository.findByUsername(username).isPresent()) { if (userRepository.findByUsername(username).isPresent()) {
log.warn("Registration rejected: username already exists ({})", username); log.warn("Registration rejected: username already exists ({})", username);
Map<String, String> error = new HashMap<>(); throw new ConflictException("Username already exists");
error.put("message", "Username already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
} }
if (userRepository.findByEmail(email).isPresent()) { if (userRepository.findByEmail(email).isPresent()) {
log.warn("Registration rejected: email already exists"); log.warn("Registration rejected: email already exists");
Map<String, String> error = new HashMap<>(); throw new ConflictException("Email already exists");
error.put("message", "Email already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
} }
if (phone != null && userRepository.findByPhone(phone).isPresent()) { if (phone != null && userRepository.findByPhone(phone).isPresent()) {
log.warn("Registration rejected: phone already exists"); log.warn("Registration rejected: phone already exists");
Map<String, String> error = new HashMap<>(); throw new ConflictException("Phone already exists");
error.put("message", "Phone already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
} }
User user = new User(); User user = new User();
@@ -117,9 +114,7 @@ public class AuthController {
try { try {
savedUser = userRepository.save(user); savedUser = userRepository.save(user);
} catch (DataIntegrityViolationException e) { } catch (DataIntegrityViolationException e) {
Map<String, String> error = new HashMap<>(); throw new ConflictException("Username, email, or phone already exists");
error.put("message", "Username, email, or phone already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
} }
emailService.sendWelcome(savedUser); emailService.sendWelcome(savedUser);
@@ -137,7 +132,7 @@ public class AuthController {
} }
@PostMapping("/login") @PostMapping("/login")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) { public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
try { try {
authenticationManager.authenticate( authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()) new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
@@ -159,21 +154,15 @@ public class AuthController {
} catch (BadCredentialsException e) { } catch (BadCredentialsException e) {
log.warn("Login failed for username {}", request.getUsername()); log.warn("Login failed for username {}", request.getUsername());
Map<String, String> error = new HashMap<>(); throw e;
error.put("message", "Invalid username or password");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
} catch (InternalAuthenticationServiceException e) { } catch (InternalAuthenticationServiceException e) {
if (e.getCause() instanceof DisabledException disabledException) { if (e.getCause() instanceof DisabledException disabledException) {
log.warn("Login denied for disabled user {}", request.getUsername()); log.warn("Login denied for disabled user {}", request.getUsername());
Map<String, String> error = new HashMap<>(); throw disabledException;
error.put("message", disabledException.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
} }
throw e; throw e;
} catch (DisabledException e) { } catch (DisabledException e) {
Map<String, String> error = new HashMap<>(); throw e;
error.put("message", e.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
} }
} }
@@ -196,7 +185,7 @@ public class AuthController {
@Transactional @Transactional
@PutMapping("/me") @PutMapping("/me")
public ResponseEntity<?> updateProfile(@Valid @RequestBody ProfileUpdateRequest request) { public ResponseEntity<UserInfoResponse> updateProfile(@Valid @RequestBody ProfileUpdateRequest request) {
Long userId = AuthenticationHelper.getAuthenticatedUserId(); Long userId = AuthenticationHelper.getAuthenticatedUserId();
User user = userRepository.findByIdForUpdate(userId) User user = userRepository.findByIdForUpdate(userId)
.orElseThrow(() -> new UsernameNotFoundException("User not found")); .orElseThrow(() -> new UsernameNotFoundException("User not found"));
@@ -205,9 +194,7 @@ public class AuthController {
String username = trimToNull(request.getUsername()); String username = trimToNull(request.getUsername());
if (username != null && !username.equals(user.getUsername())) { if (username != null && !username.equals(user.getUsername())) {
if (userRepository.findByUsername(username).isPresent()) { if (userRepository.findByUsername(username).isPresent()) {
Map<String, String> error = new HashMap<>(); throw new ConflictException("Username already exists");
error.put("message", "Username already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
} }
user.setUsername(username); user.setUsername(username);
invalidateToken = true; invalidateToken = true;
@@ -216,9 +203,7 @@ public class AuthController {
String email = trimToNull(request.getEmail()); String email = trimToNull(request.getEmail());
if (email != null && !email.equals(user.getEmail())) { if (email != null && !email.equals(user.getEmail())) {
if (userRepository.findByEmail(email).isPresent()) { if (userRepository.findByEmail(email).isPresent()) {
Map<String, String> error = new HashMap<>(); throw new ConflictException("Email already exists");
error.put("message", "Email already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
} }
user.setEmail(email); user.setEmail(email);
} }
@@ -241,9 +226,7 @@ public class AuthController {
if (phone != null && userRepository.findByPhone(phone) if (phone != null && userRepository.findByPhone(phone)
.filter(existing -> !existing.getId().equals(user.getId())) .filter(existing -> !existing.getId().equals(user.getId()))
.isPresent()) { .isPresent()) {
Map<String, String> error = new HashMap<>(); throw new ConflictException("Phone already exists");
error.put("message", "Phone already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
} }
user.setPhone(phone); user.setPhone(phone);
} }
@@ -262,9 +245,7 @@ public class AuthController {
try { try {
updatedUser = userRepository.save(user); updatedUser = userRepository.save(user);
} catch (DataIntegrityViolationException e) { } catch (DataIntegrityViolationException e) {
Map<String, String> error = new HashMap<>(); throw new ConflictException("Username, email, or phone already exists");
error.put("message", "Username, email, or phone already exists");
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
} }
userAuthCacheService.evict(updatedUser.getId()); userAuthCacheService.evict(updatedUser.getId());
return ResponseEntity.ok(toUserInfoResponse(updatedUser)); return ResponseEntity.ok(toUserInfoResponse(updatedUser));
@@ -306,17 +287,6 @@ public class AuthController {
return trimToNull(PhoneUtils.normalize(trimToNull(value))); 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) { private String joinFullName(String firstName, String lastName) {
String first = trimToNull(firstName); String first = trimToNull(firstName);
String last = trimToNull(lastName); String last = trimToNull(lastName);
@@ -329,30 +299,21 @@ public class AuthController {
return first + " " + last; return first + " " + last;
} }
private record NameParts(String firstName, String lastName, String fullName) {
}
@PostMapping("/me/avatar") @PostMapping("/me/avatar")
public ResponseEntity<?> uploadAvatar(@RequestParam("avatar") MultipartFile file) { public ResponseEntity<AvatarUploadResponse> uploadAvatar(@RequestParam("avatar") MultipartFile file) {
User user = getAuthenticatedUser(); User user = getAuthenticatedUser();
if (file.isEmpty()) { if (file.isEmpty()) {
Map<String, String> error = new HashMap<>(); throw new BusinessException("Please select a file to upload");
error.put("message", "Please select a file to upload");
return ResponseEntity.badRequest().body(error);
} }
if (file.getSize() > 5 * 1024 * 1024) { if (file.getSize() > 5 * 1024 * 1024) {
Map<String, String> error = new HashMap<>(); throw new BusinessException("File size must not exceed 5MB");
error.put("message", "File size must not exceed 5MB");
return ResponseEntity.badRequest().body(error);
} }
String contentType = file.getContentType(); String contentType = file.getContentType();
if (contentType == null || (!contentType.equals("image/jpeg") && !contentType.equals("image/png") && !contentType.equals("image/gif"))) { if (contentType == null || (!contentType.equals("image/jpeg") && !contentType.equals("image/png") && !contentType.equals("image/gif"))) {
Map<String, String> error = new HashMap<>(); throw new BusinessException("Only JPG, PNG, and GIF images are allowed");
error.put("message", "Only JPG, PNG, and GIF images are allowed");
return ResponseEntity.badRequest().body(error);
} }
try { try {
@@ -364,9 +325,7 @@ public class AuthController {
return ResponseEntity.ok(new AvatarUploadResponse(avatarStorageService.toOwnerAvatarUrl(user), "Avatar uploaded successfully")); return ResponseEntity.ok(new AvatarUploadResponse(avatarStorageService.toOwnerAvatarUrl(user), "Avatar uploaded successfully"));
} catch (IOException e) { } catch (IOException e) {
Map<String, String> error = new HashMap<>(); throw new BusinessException("Failed to upload avatar: " + e.getMessage());
error.put("message", "Failed to upload avatar: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
} }
} }
@@ -375,9 +334,7 @@ public class AuthController {
User user = getAuthenticatedUser(); User user = getAuthenticatedUser();
if (!avatarStorageService.hasAvatar(user)) { if (!avatarStorageService.hasAvatar(user)) {
Map<String, String> error = new HashMap<>(); throw new ResourceNotFoundException("No avatar uploaded");
error.put("message", "No avatar uploaded");
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
} }
Map<String, String> response = new HashMap<>(); Map<String, String> response = new HashMap<>();

View File

@@ -0,0 +1,7 @@
package com.petshop.backend.exception;
public class ConflictException extends RuntimeException {
public ConflictException(String message) {
super(message);
}
}

View File

@@ -5,6 +5,8 @@ import jakarta.servlet.http.HttpServletRequest;
import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.core.PropertyReferenceException; import org.springframework.data.core.PropertyReferenceException;
import org.springframework.http.HttpStatus; 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.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
@@ -33,6 +35,11 @@ public class GlobalExceptionHandler {
return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), ex, request); return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), ex, request);
} }
@ExceptionHandler(ConflictException.class)
public ResponseEntity<ApiErrorResponse> handleConflictException(ConflictException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.CONFLICT, ex.getMessage(), ex, request);
}
@ExceptionHandler(MethodArgumentNotValidException.class) @ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) { public ResponseEntity<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) {
Map<String, String> errors = new HashMap<>(); Map<String, String> errors = new HashMap<>();
@@ -54,6 +61,16 @@ public class GlobalExceptionHandler {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
} }
@ExceptionHandler(BadCredentialsException.class)
public ResponseEntity<ApiErrorResponse> handleBadCredentials(BadCredentialsException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.UNAUTHORIZED, "Invalid username or password", ex, request);
}
@ExceptionHandler(DisabledException.class)
public ResponseEntity<ApiErrorResponse> handleDisabledException(DisabledException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.FORBIDDEN, ex.getMessage(), ex, request);
}
@ExceptionHandler(org.springframework.security.access.AccessDeniedException.class) @ExceptionHandler(org.springframework.security.access.AccessDeniedException.class)
public ResponseEntity<ApiErrorResponse> handleAccessDeniedException(org.springframework.security.access.AccessDeniedException ex, HttpServletRequest request) { public ResponseEntity<ApiErrorResponse> handleAccessDeniedException(org.springframework.security.access.AccessDeniedException ex, HttpServletRequest request) {
return buildErrorResponse(HttpStatus.FORBIDDEN, ex.getMessage(), ex, request); return buildErrorResponse(HttpStatus.FORBIDDEN, ex.getMessage(), ex, request);

View File

@@ -8,6 +8,7 @@ import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.Sale; import com.petshop.backend.entity.Sale;
import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User; import com.petshop.backend.entity.User;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.event.AdoptionConfirmedEvent; import com.petshop.backend.event.AdoptionConfirmedEvent;
import com.petshop.backend.event.SaleReceiptEvent; import com.petshop.backend.event.SaleReceiptEvent;
@@ -96,7 +97,7 @@ public class AdoptionService {
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus()); String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
validatePetAvailability(pet, null, null); validatePetAvailability(pet, null, null);
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) && request.getAdoptionDate() != null && request.getAdoptionDate().isAfter(LocalDate.now())) { 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(); Adoption adoption = new Adoption();
@@ -139,7 +140,7 @@ public class AdoptionService {
Long currentPetId = adoption.getPet() != null ? adoption.getPet().getPetId() : null; Long currentPetId = adoption.getPet() != null ? adoption.getPet().getPetId() : null;
validatePetAvailability(pet, adoption.getAdoptionId(), currentPetId); validatePetAvailability(pet, adoption.getAdoptionId(), currentPetId);
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus) && request.getAdoptionDate() != null && request.getAdoptionDate().isAfter(LocalDate.now())) { 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); adoption.setPet(pet);
@@ -168,7 +169,7 @@ public class AdoptionService {
// Verify the pet is actually located at the claimed store // Verify the pet is actually located at the claimed store
if (pet.getStore() == null || !pet.getStore().getStoreId().equals(sourceStoreId)) { 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 // Verify the pet is available for adoption
@@ -203,7 +204,7 @@ public class AdoptionService {
} }
if (!ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoption.getAdoptionStatus())) { 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); adoption.setAdoptionStatus(ADOPTION_STATUS_CANCELLED);
@@ -280,7 +281,7 @@ public class AdoptionService {
User employee = userRepository.findById(requestedEmployeeId) User employee = userRepository.findById(requestedEmployeeId)
.orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId)); .orElseThrow(() -> new ResourceNotFoundException("Employee not found with id: " + requestedEmployeeId));
if (!isAssignableUser(employee)) { 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; return employee;
} }
@@ -295,7 +296,7 @@ public class AdoptionService {
private String normalizeAdoptionStatus(String adoptionStatus) { private String normalizeAdoptionStatus(String adoptionStatus) {
if (adoptionStatus == null) { if (adoptionStatus == null) {
throw new IllegalArgumentException("Adoption status is required"); throw new BusinessException("Adoption status is required");
} }
String trimmedStatus = adoptionStatus.trim(); String trimmedStatus = adoptionStatus.trim();
if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(trimmedStatus)) { if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(trimmedStatus)) {
@@ -310,7 +311,7 @@ public class AdoptionService {
if (ADOPTION_STATUS_MISSED.equalsIgnoreCase(trimmedStatus)) { if (ADOPTION_STATUS_MISSED.equalsIgnoreCase(trimmedStatus)) {
return ADOPTION_STATUS_MISSED; 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) { 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_IdAndAdoptionStatusIgnoreCase(pet.getPetId(), ADOPTION_STATUS_COMPLETED)
: adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId); : adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCaseAndAdoptionIdNot(pet.getPetId(), ADOPTION_STATUS_COMPLETED, adoptionId);
if (adoptedElsewhere) { 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())) { 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");
} }
} }

View File

@@ -116,14 +116,14 @@ public class AppointmentService {
// Customers must supply a pet that is Adopted and owned by them // Customers must supply a pet that is Adopted and owned by them
if (User.Role.CUSTOMER.equals(authenticatedUser.getRole())) { if (User.Role.CUSTOMER.equals(authenticatedUser.getRole())) {
if (pet == null) { 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())) { 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(); String petStatus = pet.getPetStatus();
if (!"Owned".equalsIgnoreCase(petStatus) && !"Adopted".equalsIgnoreCase(petStatus)) { 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(); String status = appointment.getAppointmentStatus();
if (!"Booked".equalsIgnoreCase(status) && !"Scheduled".equalsIgnoreCase(status)) { 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"); appointment.setAppointmentStatus("Cancelled");
@@ -311,7 +311,7 @@ public class AppointmentService {
if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) { if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) {
LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime()); LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime());
if (appointmentDateTime.isBefore(LocalDateTime.now())) { 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() boolean assignedToStore = assignableUsers.stream()
.anyMatch(u -> u.getId().equals(requestedEmployeeId)); .anyMatch(u -> u.getId().equals(requestedEmployeeId));
if (!assignedToStore) { 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; return employee;
} }
@@ -377,7 +377,7 @@ public class AppointmentService {
List<Appointment> existingAppointments = appointmentRepository List<Appointment> existingAppointments = appointmentRepository
.findByEmployeeIdAndAppointmentDate(employee.getId(), date); .findByEmployeeIdAndAppointmentDate(employee.getId(), date);
if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { 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<Appointment> existingAppointments = appointmentRepository List<Appointment> existingAppointments = appointmentRepository
.findByPetIdAndAppointmentDate(pet.getPetId(), date); .findByPetIdAndAppointmentDate(pet.getPetId(), date);
if (!isSlotAvailable(existingAppointments, service, time, appointmentIdToIgnore)) { 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");
} }
} }

View File

@@ -4,6 +4,7 @@ import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.dto.common.CouponRequest; import com.petshop.backend.dto.common.CouponRequest;
import com.petshop.backend.dto.common.CouponResponse; import com.petshop.backend.dto.common.CouponResponse;
import com.petshop.backend.entity.Coupon; import com.petshop.backend.entity.Coupon;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.CouponRepository; import com.petshop.backend.repository.CouponRepository;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
@@ -39,7 +40,7 @@ public class CouponService {
@Transactional @Transactional
public CouponResponse createCoupon(CouponRequest request) { public CouponResponse createCoupon(CouponRequest request) {
if (couponRepository.findByCouponCode(request.getCouponCode()).isPresent()) { 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(); Coupon coupon = new Coupon();
@@ -55,7 +56,7 @@ public class CouponService {
couponRepository.findByCouponCode(request.getCouponCode()).ifPresent(existing -> { couponRepository.findByCouponCode(request.getCouponCode()).ifPresent(existing -> {
if (!existing.getCouponId().equals(id)) { if (!existing.getCouponId().equals(id)) {
throw new IllegalArgumentException("Coupon code already exists: " + request.getCouponCode()); throw new BusinessException("Coupon code already exists: " + request.getCouponCode());
} }
}); });

View File

@@ -9,6 +9,7 @@ import com.petshop.backend.entity.Adoption;
import com.petshop.backend.entity.Pet; import com.petshop.backend.entity.Pet;
import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User; import com.petshop.backend.entity.User;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.security.AppPrincipal; import com.petshop.backend.security.AppPrincipal;
import com.petshop.backend.repository.AdoptionRepository; import com.petshop.backend.repository.AdoptionRepository;
@@ -125,7 +126,7 @@ public class PetService {
boolean hasBooked = linkedAppointments.stream() boolean hasBooked = linkedAppointments.stream()
.anyMatch(a -> "Booked".equalsIgnoreCase(a.getAppointmentStatus())); .anyMatch(a -> "Booked".equalsIgnoreCase(a.getAppointmentStatus()));
if (hasBooked) { 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."); "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 // 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) { private void validateImageFile(MultipartFile file) {
if (file == null || file.isEmpty()) { 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) { 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(); String contentType = file.getContentType();
if (contentType == null) { 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); String normalized = contentType.toLowerCase(Locale.ROOT);
if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) { 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");
} }
} }

View File

@@ -135,18 +135,18 @@ public class ProductService {
private void validateImageFile(MultipartFile file) { private void validateImageFile(MultipartFile file) {
if (file == null || file.isEmpty()) { 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) { 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(); String contentType = file.getContentType();
if (contentType == null) { 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); String normalized = contentType.toLowerCase(Locale.ROOT);
if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) { 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");
} }
} }

View File

@@ -3,8 +3,11 @@ package com.petshop.backend.util;
import com.petshop.backend.entity.User; import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository; import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.AppPrincipal; 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.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@@ -13,7 +16,7 @@ public class AuthenticationHelper {
public static Authentication getAuthentication() { public static Authentication getAuthentication() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) { if (authentication == null || !authentication.isAuthenticated()) {
throw new RuntimeException("No authenticated user found"); throw new AuthenticationCredentialsNotFoundException("No authenticated user found");
} }
return authentication; return authentication;
} }
@@ -23,7 +26,7 @@ public class AuthenticationHelper {
if (principal instanceof AppPrincipal appPrincipal) { if (principal instanceof AppPrincipal appPrincipal) {
return appPrincipal; return appPrincipal;
} }
throw new RuntimeException("Authenticated principal is not supported"); throw new AuthenticationServiceException("Authenticated principal is not supported");
} }
public static Long getAuthenticatedUserId() { public static Long getAuthenticatedUserId() {
@@ -36,11 +39,11 @@ public class AuthenticationHelper {
if (principal instanceof AppPrincipal appPrincipal) { if (principal instanceof AppPrincipal appPrincipal) {
return userRepository.findById(appPrincipal.getUserId()) 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(); String username = authentication.getName();
return userRepository.findByUsername(username) return userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found: " + username)); .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
} }
} }