Merge pull request #324 from RecentRunner/refactor/backend-cleanup
backend DRY/KISS cleanup
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -14,8 +12,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;
|
||||
@@ -25,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
|
||||
@@ -41,17 +37,8 @@ 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 = authHelper.getEffectiveCustomerId(customerId);
|
||||
|
||||
LocalDate adoptionDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null;
|
||||
|
||||
@@ -61,18 +48,7 @@ public class AdoptionController {
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||
public ResponseEntity<AdoptionResponse> 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 = authHelper.getCustomerIdOrNull();
|
||||
return ResponseEntity.ok(adoptionService.getAdoptionById(id, customerId));
|
||||
}
|
||||
|
||||
@@ -85,27 +61,15 @@ public class AdoptionController {
|
||||
@PostMapping("/request")
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')")
|
||||
public ResponseEntity<AdoptionResponse> requestAdoption(@Valid @RequestBody CustomerAdoptionRequest request) {
|
||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(
|
||||
adoptionService.requestAdoption(user.getId(), request.getPetId(), request.getEmployeeId(), request.getSourceStoreId(), request.getAdoptionDate())
|
||||
adoptionService.requestAdoption(authHelper.getAuthenticatedUser().getId(), request.getPetId(), request.getEmployeeId(), request.getSourceStoreId(), request.getAdoptionDate())
|
||||
);
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}/cancel")
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||
public ResponseEntity<AdoptionResponse> cancelAdoption(@PathVariable Long id) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
String role = authentication.getAuthorities().stream()
|
||||
.findFirst()
|
||||
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
|
||||
.orElse(null);
|
||||
|
||||
Long customerId = null;
|
||||
if ("CUSTOMER".equals(role)) {
|
||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||
customerId = user.getId();
|
||||
}
|
||||
|
||||
Long customerId = authHelper.getCustomerIdOrNull();
|
||||
return ResponseEntity.ok(adoptionService.cancelAdoption(id, customerId));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,12 @@ import com.petshop.backend.dto.ai.AiChatResponse;
|
||||
import com.petshop.backend.entity.Pet;
|
||||
import com.petshop.backend.entity.User;
|
||||
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,24 +22,14 @@ 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")
|
||||
@@ -52,15 +40,13 @@ public class AiChatController {
|
||||
}
|
||||
ContentFilter.validate(request.getMessage());
|
||||
|
||||
User user = getCurrentUser();
|
||||
User user = authHelper.getAuthenticatedUser();
|
||||
|
||||
List<Pet> userPets;
|
||||
try {
|
||||
userPets = petRepository.findAllByOwner_IdAndPetStatusInOrderByPetNameAsc(
|
||||
user.getId(), List.of("Adopted", "Owned"));
|
||||
}
|
||||
|
||||
catch (Exception e) {
|
||||
} catch (Exception e) {
|
||||
userPets = Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -72,15 +58,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."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -13,8 +11,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;
|
||||
@@ -25,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
|
||||
@@ -43,17 +39,7 @@ 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 = authHelper.getEffectiveCustomerId(customerId);
|
||||
|
||||
LocalDate appointmentDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null;
|
||||
|
||||
@@ -64,33 +50,15 @@ public class AppointmentController {
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||
public ResponseEntity<AppointmentResponse> 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 = authHelper.getCustomerIdOrNull();
|
||||
return ResponseEntity.ok(appointmentService.getAppointmentById(id, customerId));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||
public ResponseEntity<AppointmentResponse> 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)) {
|
||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||
if (!request.getCustomerId().equals(user.getId())) {
|
||||
if (AuthenticationHelper.isCustomer()) {
|
||||
if (!request.getCustomerId().equals(authHelper.getAuthenticatedUser().getId())) {
|
||||
throw new org.springframework.security.access.AccessDeniedException("You can only create appointments for yourself");
|
||||
}
|
||||
}
|
||||
@@ -101,18 +69,7 @@ public class AppointmentController {
|
||||
@PatchMapping("/{id}/cancel")
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||
public ResponseEntity<AppointmentResponse> cancelAppointment(@PathVariable Long id) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
String role = authentication.getAuthorities().stream()
|
||||
.findFirst()
|
||||
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
|
||||
.orElse(null);
|
||||
|
||||
Long customerId = null;
|
||||
if ("CUSTOMER".equals(role)) {
|
||||
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||
customerId = user.getId();
|
||||
}
|
||||
|
||||
Long customerId = authHelper.getCustomerIdOrNull();
|
||||
return ResponseEntity.ok(appointmentService.cancelAppointment(id, customerId));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -21,7 +24,8 @@ 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.PhoneUtils;
|
||||
import com.petshop.backend.util.ImageValidationUtil;
|
||||
import com.petshop.backend.util.StringUtils;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -60,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;
|
||||
@@ -71,35 +76,30 @@ public class AuthController {
|
||||
this.passwordResetService = passwordResetService;
|
||||
this.emailService = emailService;
|
||||
this.userAuthCacheService = userAuthCacheService;
|
||||
this.authHelper = authHelper;
|
||||
}
|
||||
|
||||
@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 phone = normalizePhone(request.getPhone());
|
||||
public ResponseEntity<RegisterResponse> register(@Valid @RequestBody RegisterRequest request) {
|
||||
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 = StringUtils.normalizePhone(request.getPhone());
|
||||
|
||||
if (userRepository.findByUsername(username).isPresent()) {
|
||||
log.warn("Registration rejected: username already exists ({})", username);
|
||||
Map<String, String> 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<String, String> 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<String, String> 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();
|
||||
@@ -108,7 +108,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);
|
||||
@@ -117,9 +117,7 @@ public class AuthController {
|
||||
try {
|
||||
savedUser = userRepository.save(user);
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
Map<String, String> 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 +135,7 @@ public class AuthController {
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest request) {
|
||||
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
try {
|
||||
authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword())
|
||||
@@ -159,21 +157,15 @@ public class AuthController {
|
||||
|
||||
} catch (BadCredentialsException e) {
|
||||
log.warn("Login failed for username {}", request.getUsername());
|
||||
Map<String, String> 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<String, String> error = new HashMap<>();
|
||||
error.put("message", disabledException.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
|
||||
throw disabledException;
|
||||
}
|
||||
throw e;
|
||||
} catch (DisabledException e) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,60 +182,54 @@ public class AuthController {
|
||||
@Transactional(readOnly = true)
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<UserInfoResponse> getCurrentUser() {
|
||||
User user = getAuthenticatedUser();
|
||||
User user = authHelper.getAuthenticatedUser();
|
||||
return ResponseEntity.ok(toUserInfoResponse(user));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@PutMapping("/me")
|
||||
public ResponseEntity<?> updateProfile(@Valid @RequestBody ProfileUpdateRequest request) {
|
||||
public ResponseEntity<UserInfoResponse> updateProfile(@Valid @RequestBody ProfileUpdateRequest request) {
|
||||
Long userId = AuthenticationHelper.getAuthenticatedUserId();
|
||||
User user = userRepository.findByIdForUpdate(userId)
|
||||
.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()) {
|
||||
Map<String, String> 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;
|
||||
}
|
||||
|
||||
String email = trimToNull(request.getEmail());
|
||||
String email = StringUtils.trimToNull(request.getEmail());
|
||||
if (email != null && !email.equals(user.getEmail())) {
|
||||
if (userRepository.findByEmail(email).isPresent()) {
|
||||
Map<String, String> 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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
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()))
|
||||
.isPresent()) {
|
||||
Map<String, String> 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 +248,7 @@ public class AuthController {
|
||||
try {
|
||||
updatedUser = userRepository.save(user);
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
Map<String, String> 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));
|
||||
@@ -275,7 +259,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(),
|
||||
@@ -294,90 +278,26 @@ 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)));
|
||||
}
|
||||
|
||||
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);
|
||||
if (first == null) {
|
||||
return last == null ? null : last;
|
||||
}
|
||||
if (last == null) {
|
||||
return first;
|
||||
}
|
||||
return first + " " + last;
|
||||
}
|
||||
|
||||
private record NameParts(String firstName, String lastName, String fullName) {
|
||||
}
|
||||
|
||||
@PostMapping("/me/avatar")
|
||||
public ResponseEntity<?> uploadAvatar(@RequestParam("avatar") MultipartFile file) {
|
||||
User user = getAuthenticatedUser();
|
||||
public ResponseEntity<AvatarUploadResponse> uploadAvatar(@RequestParam("avatar") MultipartFile file) throws IOException {
|
||||
User user = authHelper.getAuthenticatedUser();
|
||||
|
||||
if (file.isEmpty()) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", "Please select a file to upload");
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
}
|
||||
ImageValidationUtil.validate(file);
|
||||
|
||||
if (file.getSize() > 5 * 1024 * 1024) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", "File size must not exceed 5MB");
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
}
|
||||
avatarStorageService.deleteAvatar(user);
|
||||
String avatarPath = avatarStorageService.storeAvatar(file);
|
||||
user.setAvatarUrl(avatarPath);
|
||||
userRepository.save(user);
|
||||
|
||||
String contentType = file.getContentType();
|
||||
if (contentType == null || (!contentType.equals("image/jpeg") && !contentType.equals("image/png") && !contentType.equals("image/gif"))) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", "Only JPG, PNG, and GIF images are allowed");
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
}
|
||||
|
||||
try {
|
||||
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) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", "Failed to upload avatar: " + e.getMessage());
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
|
||||
}
|
||||
return ResponseEntity.ok(new AvatarUploadResponse(avatarStorageService.toOwnerAvatarUrl(user), "Avatar uploaded successfully"));
|
||||
}
|
||||
|
||||
@GetMapping("/me/avatar")
|
||||
public ResponseEntity<?> getAvatar() {
|
||||
User user = getAuthenticatedUser();
|
||||
User user = authHelper.getAuthenticatedUser();
|
||||
|
||||
if (!avatarStorageService.hasAvatar(user)) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", "No avatar uploaded");
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
|
||||
throw new ResourceNotFoundException("No avatar uploaded");
|
||||
}
|
||||
|
||||
Map<String, String> response = new HashMap<>();
|
||||
@@ -387,7 +307,7 @@ public class AuthController {
|
||||
|
||||
@GetMapping("/me/avatar/file")
|
||||
public ResponseEntity<Resource> getAvatarFile() {
|
||||
User user = getAuthenticatedUser();
|
||||
User user = authHelper.getAuthenticatedUser();
|
||||
|
||||
if (!avatarStorageService.hasAvatar(user)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -404,12 +324,13 @@ public class AuthController {
|
||||
|
||||
@DeleteMapping("/me/avatar")
|
||||
public ResponseEntity<?> deleteAvatar() {
|
||||
User user = getAuthenticatedUser();
|
||||
User user = authHelper.getAuthenticatedUser();
|
||||
|
||||
if (avatarStorageService.hasAvatar(user)) {
|
||||
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);
|
||||
@@ -428,11 +349,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ConversationResponse> 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<List<ConversationResponse>> getConversations(
|
||||
@RequestParam(required = false, defaultValue = "false") boolean mine) {
|
||||
User user = getCurrentUser();
|
||||
User user = authHelper.getAuthenticatedUser();
|
||||
List<ConversationResponse> 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<ConversationResponse> 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<MessageResponse> 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<Resource> 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<List<MessageResponse>> getMessages(@PathVariable Long id) {
|
||||
User user = getCurrentUser();
|
||||
User user = authHelper.getAuthenticatedUser();
|
||||
List<MessageResponse> 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<ConversationResponse> 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<ConversationResponse> 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);
|
||||
|
||||
@@ -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<List<DropdownOption>> 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())
|
||||
);
|
||||
|
||||
@@ -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<List<MyPetResponse>> getMyPets(@RequestParam(required = false) String status) {
|
||||
return ResponseEntity.ok(petService.getMyPets(currentUserId(), status));
|
||||
return ResponseEntity.ok(petService.getMyPets(AuthenticationHelper.getAuthenticatedUserId(), status));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<MyPetResponse> createMyPet(@Valid @RequestBody MyPetRequest request) {
|
||||
return ResponseEntity.ok(petService.createMyPet(currentUserId(), request));
|
||||
return ResponseEntity.ok(petService.createMyPet(AuthenticationHelper.getAuthenticatedUserId(), request));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<MyPetResponse> 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<Void> 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<MyPetResponse> uploadMyPetImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) throws IOException {
|
||||
return ResponseEntity.ok(petService.uploadMyPetImage(AuthenticationHelper.getAuthenticatedUserId(), id, image));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PetResponse> uploadPetImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) throws IOException {
|
||||
return ResponseEntity.ok(petService.uploadPetImage(id, image));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/image")
|
||||
public ResponseEntity<Resource> 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<PetResponse> deletePetImage(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(petService.deletePetImage(id));
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, String>> badRequest(String message) {
|
||||
Map<String, String> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<ProductResponse> 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<ProductResponse> deleteProductImage(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(productService.deleteProductImage(id));
|
||||
}
|
||||
|
||||
private ResponseEntity<Map<String, String>> badRequest(String message) {
|
||||
Map<String, String> error = new HashMap<>();
|
||||
error.put("message", message);
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,16 +3,12 @@ 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;
|
||||
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;
|
||||
@@ -22,46 +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<RefundResponse> 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 = authHelper.getCustomerIdOrNull();
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(refundService.createRefund(request, customerId));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||
public ResponseEntity<List<RefundResponse>> 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 = authHelper.getCustomerIdOrNull();
|
||||
List<RefundResponse> refunds = refundService.getAllRefunds(customerId);
|
||||
return ResponseEntity.ok(refunds);
|
||||
}
|
||||
@@ -69,18 +43,7 @@ public class RefundController {
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||
public ResponseEntity<RefundResponse> 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 = authHelper.getCustomerIdOrNull();
|
||||
return ResponseEntity.ok(refundService.getRefundById(id, customerId));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, String> error = new HashMap<>();
|
||||
error.put("message", "Please select a file to upload");
|
||||
return ResponseEntity.badRequest().body(error);
|
||||
}
|
||||
|
||||
if (file.getSize() > 5 * 1024 * 1024) {
|
||||
Map<String, String> 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<String, String> 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<String, String> 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<String, String> result = new HashMap<>();
|
||||
result.put("message", "Avatar removed successfully");
|
||||
return ResponseEntity.ok(result);
|
||||
} catch (IOException e) {
|
||||
Map<String, String> 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<String, String> result = new HashMap<>();
|
||||
result.put("message", "Avatar removed successfully");
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.petshop.backend.exception;
|
||||
|
||||
public class ConflictException extends RuntimeException {
|
||||
public ConflictException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,12 @@ 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;
|
||||
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 +36,11 @@ public class GlobalExceptionHandler {
|
||||
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)
|
||||
public ResponseEntity<Map<String, Object>> handleValidationExceptions(MethodArgumentNotValidException ex, HttpServletRequest request) {
|
||||
Map<String, String> errors = new HashMap<>();
|
||||
@@ -54,6 +62,16 @@ public class GlobalExceptionHandler {
|
||||
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)
|
||||
public ResponseEntity<ApiErrorResponse> handleAccessDeniedException(org.springframework.security.access.AccessDeniedException ex, HttpServletRequest request) {
|
||||
return buildErrorResponse(HttpStatus.FORBIDDEN, ex.getMessage(), ex, request);
|
||||
@@ -104,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<ApiErrorResponse> handleIOException(IOException ex, HttpServletRequest request) {
|
||||
return buildErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage(), ex, request);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ApiErrorResponse> handleGenericException(Exception ex, HttpServletRequest request) {
|
||||
String message = ex.getMessage() == null || ex.getMessage().isBlank()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -8,6 +9,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;
|
||||
@@ -54,9 +56,10 @@ public class AdoptionService {
|
||||
this.eventPublisher = eventPublisher;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<AdoptionResponse> 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<Adoption> adoptions = adoptionRepository.searchAdoptions(
|
||||
normalizedQuery,
|
||||
@@ -70,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));
|
||||
@@ -96,7 +100,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 +143,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 +172,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 +207,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);
|
||||
@@ -247,14 +251,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(
|
||||
@@ -262,9 +258,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(),
|
||||
@@ -280,7 +276,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 +291,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 +306,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 +315,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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,9 @@ 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;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -46,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;
|
||||
@@ -55,6 +58,7 @@ public class AppointmentService {
|
||||
this.userRepository = userRepository;
|
||||
this.adoptionRepository = adoptionRepository;
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.businessProperties = businessProperties;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@@ -67,8 +71,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<Appointment> appointments = appointmentRepository.searchAppointments(
|
||||
normalizedQuery,
|
||||
@@ -116,14 +120,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 +202,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");
|
||||
@@ -239,8 +243,8 @@ public class AppointmentService {
|
||||
.collect(Collectors.groupingBy(a -> a.getEmployee().getId()));
|
||||
|
||||
List<String> 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;
|
||||
@@ -254,7 +258,7 @@ public class AppointmentService {
|
||||
if (anyEmployeeAvailable) {
|
||||
availableSlots.add(currentTime.toString());
|
||||
}
|
||||
currentTime = currentTime.plusMinutes(30);
|
||||
currentTime = currentTime.plusMinutes(businessProperties.slotIntervalMinutes());
|
||||
}
|
||||
|
||||
return availableSlots;
|
||||
@@ -288,14 +292,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<String> allowed = service.getSpecies();
|
||||
@@ -311,7 +307,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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -326,17 +322,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(StringUtils.fullName(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(StringUtils.fullName(appointment.getEmployee().getFirstName(), appointment.getEmployee().getLastName()));
|
||||
}
|
||||
response.setPetName(pet != null ? pet.getPetName() : null);
|
||||
response.setPetId(pet != null ? pet.getPetId() : null);
|
||||
response.setCreatedAt(appointment.getCreatedAt());
|
||||
@@ -355,7 +359,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;
|
||||
}
|
||||
@@ -369,7 +373,7 @@ public class AppointmentService {
|
||||
List<Appointment> 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +397,7 @@ public class AppointmentService {
|
||||
List<Appointment> 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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,13 @@ 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.config.BusinessProperties;
|
||||
import com.stripe.Stripe;
|
||||
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;
|
||||
|
||||
@@ -24,16 +25,16 @@ import java.util.List;
|
||||
@Service
|
||||
public class CartService {
|
||||
|
||||
private static final int LOYALTY_POINTS_PER_DOLLAR = 20;
|
||||
|
||||
private final CartRepository cartRepository;
|
||||
private final CartItemRepository cartItemRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
private final ProductRepository productRepository;
|
||||
private final CouponRepository couponRepository;
|
||||
private final CouponService couponService;
|
||||
private final SaleRepository saleRepository;
|
||||
private final SaleService saleService;
|
||||
private final BusinessProperties businessProperties;
|
||||
|
||||
@Value("${stripe.secret-key:}")
|
||||
private String stripeSecretKey;
|
||||
@@ -44,16 +45,20 @@ public class CartService {
|
||||
StoreRepository storeRepository,
|
||||
ProductRepository productRepository,
|
||||
CouponRepository couponRepository,
|
||||
CouponService couponService,
|
||||
SaleRepository saleRepository,
|
||||
SaleService saleService) {
|
||||
SaleService saleService,
|
||||
BusinessProperties businessProperties) {
|
||||
this.cartRepository = cartRepository;
|
||||
this.cartItemRepository = cartItemRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
this.productRepository = productRepository;
|
||||
this.couponRepository = couponRepository;
|
||||
this.couponService = couponService;
|
||||
this.saleRepository = saleRepository;
|
||||
this.saleService = saleService;
|
||||
this.businessProperties = businessProperties;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
@@ -188,31 +193,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,80 +436,45 @@ 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<CartItem> 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<CartItem> 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
@@ -20,11 +21,13 @@ public class CategoryService {
|
||||
this.categoryRepository = categoryRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<CategoryResponse> 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);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public CategoryResponse getCategoryById(Long id) {
|
||||
Category category = categoryRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + id));
|
||||
@@ -76,11 +79,4 @@ public class CategoryService {
|
||||
);
|
||||
}
|
||||
|
||||
private String normalizeFilter(String value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String trimmed = value.trim();
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,32 +4,43 @@ 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 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;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<CouponResponse> 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));
|
||||
@@ -39,7 +50,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 +66,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());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -88,6 +99,57 @@ 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(new BigDecimal("100"), 4, 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) {
|
||||
if (discountType == null) return false;
|
||||
String type = discountType.trim();
|
||||
return "PERCENTAGE".equalsIgnoreCase(type) || "PERCENT".equalsIgnoreCase(type);
|
||||
}
|
||||
|
||||
public static boolean isFixedType(String discountType) {
|
||||
if (discountType == null) return false;
|
||||
String type = discountType.trim();
|
||||
return "FIXED".equalsIgnoreCase(type) || "FLAT".equalsIgnoreCase(type);
|
||||
}
|
||||
|
||||
private CouponResponse mapToResponse(Coupon coupon) {
|
||||
return new CouponResponse(
|
||||
coupon.getCouponId(),
|
||||
|
||||
@@ -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 """
|
||||
<div style="font-family:sans-serif;max-width:600px;margin:auto">
|
||||
|
||||
@@ -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;
|
||||
@@ -28,12 +29,14 @@ public class InventoryService {
|
||||
this.storeRepository = storeRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<InventoryResponse> getAllInventory(String query, Long storeId, Pageable pageable) {
|
||||
String normalizedQuery = normalizeFilter(query);
|
||||
String normalizedQuery = StringUtils.trimToNull(query);
|
||||
Page<Inventory> 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));
|
||||
@@ -93,14 +96,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(
|
||||
|
||||
@@ -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<String> 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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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;
|
||||
import com.petshop.backend.dto.pet.PetRequest;
|
||||
@@ -9,6 +11,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;
|
||||
@@ -29,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 {
|
||||
@@ -52,10 +55,10 @@ public class PetService {
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<PetResponse> 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<Pet> pets;
|
||||
@@ -125,7 +128,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
|
||||
@@ -133,7 +136,7 @@ public class PetService {
|
||||
appt.setPet(null);
|
||||
}
|
||||
appointmentRepository.saveAll(linkedAppointments);
|
||||
deleteStoredImageIfPresent(pet.getImageUrl());
|
||||
catalogImageStorageService.deletePetImageIfPresent(pet.getImageUrl());
|
||||
petRepository.delete(pet);
|
||||
}
|
||||
|
||||
@@ -141,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));
|
||||
}
|
||||
@@ -182,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());
|
||||
}
|
||||
|
||||
@@ -196,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));
|
||||
}
|
||||
@@ -204,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));
|
||||
}
|
||||
@@ -280,19 +283,9 @@ public class PetService {
|
||||
|
||||
private void validateImageFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("Please select an image to upload");
|
||||
}
|
||||
if (file.getSize() > 5 * 1024 * 1024) {
|
||||
throw new IllegalArgumentException("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");
|
||||
}
|
||||
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("Please select an image to upload");
|
||||
}
|
||||
ImageValidationUtil.validate(file);
|
||||
}
|
||||
|
||||
private Pet findOwnedPet(Long ownerUserId, Long petId) {
|
||||
@@ -300,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();
|
||||
}
|
||||
@@ -322,14 +305,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();
|
||||
@@ -345,7 +320,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
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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;
|
||||
import com.petshop.backend.entity.Category;
|
||||
@@ -18,7 +20,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 {
|
||||
@@ -33,11 +35,13 @@ public class ProductService {
|
||||
this.catalogImageStorageService = catalogImageStorageService;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<ProductResponse> 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);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ProductResponse getProductById(Long id) {
|
||||
Product product = productRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + id));
|
||||
@@ -87,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());
|
||||
}
|
||||
|
||||
@@ -101,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));
|
||||
}
|
||||
@@ -109,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()) {
|
||||
@@ -131,29 +136,9 @@ public class ProductService {
|
||||
|
||||
private void validateImageFile(MultipartFile file) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
throw new IllegalArgumentException("Please select an image to upload");
|
||||
}
|
||||
if (file.getSize() > 5 * 1024 * 1024) {
|
||||
throw new IllegalArgumentException("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");
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
private void deleteStoredImageIfPresent(String storedImagePath) {
|
||||
if (storedImagePath == null || storedImagePath.isBlank()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
catalogImageStorageService.deleteProductImage(storedImagePath);
|
||||
} catch (IOException | IllegalArgumentException ignored) {
|
||||
throw new BusinessException("Please select an image to upload");
|
||||
}
|
||||
ImageValidationUtil.validate(file);
|
||||
}
|
||||
|
||||
private ProductResponse mapToResponse(Product product) {
|
||||
@@ -173,11 +158,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -33,13 +34,15 @@ public class ProductSupplierService {
|
||||
this.supplierRepository = supplierRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<ProductSupplierResponse> getAllProductSuppliers(String query, Long productId, Long supplierId, Pageable pageable) {
|
||||
String normalizedQuery = normalizeFilter(query);
|
||||
String normalizedQuery = StringUtils.trimToNull(query);
|
||||
Pageable mappedPageable = mapSortProperties(pageable);
|
||||
Page<ProductSupplier> productSuppliers = productSupplierRepository.searchProductSuppliers(normalizedQuery, productId, supplierId, mappedPageable);
|
||||
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)
|
||||
@@ -97,14 +100,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;
|
||||
|
||||
@@ -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;
|
||||
@@ -8,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 {
|
||||
@@ -18,26 +20,20 @@ public class PurchaseOrderService {
|
||||
this.purchaseOrderRepository = purchaseOrderRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<PurchaseOrderResponse> getAllPurchaseOrders(String query, Long storeId, Pageable pageable) {
|
||||
String normalizedQuery = normalizeFilter(query);
|
||||
String normalizedQuery = StringUtils.trimToNull(query);
|
||||
Page<PurchaseOrder> 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));
|
||||
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(
|
||||
|
||||
@@ -8,6 +8,8 @@ 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.config.BusinessProperties;
|
||||
import com.petshop.backend.util.StringUtils;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -23,32 +25,33 @@ 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 final SaleRepository saleRepository;
|
||||
private final ProductRepository productRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
private final InventoryRepository inventoryRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final CouponRepository couponRepository;
|
||||
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, 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;
|
||||
this.inventoryRepository = inventoryRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.couponRepository = couponRepository;
|
||||
this.couponService = couponService;
|
||||
this.cartRepository = cartRepository;
|
||||
this.eventPublisher = eventPublisher;
|
||||
this.businessProperties = businessProperties;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<SaleResponse> getAllSales(String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId, Pageable pageable) {
|
||||
Page<Sale> sales = saleRepository.searchSales(normalizeFilter(query), normalizeFilter(paymentMethod), storeId, isRefund, customerId, pageable);
|
||||
Page<Sale> 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));
|
||||
@@ -247,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();
|
||||
@@ -282,52 +285,13 @@ 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;
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -339,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;
|
||||
}
|
||||
@@ -354,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) {
|
||||
@@ -367,8 +331,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(StringUtils.fullName(sale.getEmployee().getFirstName(), sale.getEmployee().getLastName()));
|
||||
}
|
||||
|
||||
if (sale.getStore() != null) {
|
||||
response.setStoreId(sale.getStore().getStoreId());
|
||||
@@ -377,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());
|
||||
@@ -416,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;
|
||||
|
||||
@@ -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<ServiceResponse> 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(),
|
||||
|
||||
@@ -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<StoreResponse> getAllStores(String query, Pageable pageable) {
|
||||
Page<StoreLocation> stores;
|
||||
if (query != null && !query.trim().isEmpty()) {
|
||||
stores = storeRepository.searchStores(query, pageable);
|
||||
} else {
|
||||
stores = storeRepository.findAll(pageable);
|
||||
}
|
||||
String q = StringUtils.trimToNull(query);
|
||||
Page<StoreLocation> 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(),
|
||||
|
||||
@@ -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<SupplierResponse> getAllSuppliers(String query, Pageable pageable) {
|
||||
Page<Supplier> suppliers;
|
||||
if (query != null && !query.trim().isEmpty()) {
|
||||
suppliers = supplierRepository.searchSuppliers(query, pageable);
|
||||
} else {
|
||||
suppliers = supplierRepository.findAll(pageable);
|
||||
}
|
||||
String q = StringUtils.trimToNull(query);
|
||||
Page<Supplier> 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(),
|
||||
|
||||
@@ -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;
|
||||
@@ -43,6 +44,7 @@ public class UserService {
|
||||
this.userAuthCacheService = userAuthCacheService;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Page<UserResponse> getAllUsers(String query, String role, Pageable pageable) {
|
||||
User.Role parsedRole = parseRole(role);
|
||||
Page<User> users;
|
||||
@@ -59,6 +61,7 @@ public class UserService {
|
||||
return users.map(this::mapToResponse);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public UserResponse getUserById(Long id) {
|
||||
return getUserById(id, null);
|
||||
}
|
||||
@@ -81,7 +84,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 +92,9 @@ public class UserService {
|
||||
user.setLastName(request.getLastName());
|
||||
user.setFullName(request.getFullName());
|
||||
user.setEmail(request.getEmail());
|
||||
user.setPhone(trimToNull(request.getPhone()));
|
||||
user.setPhone(StringUtils.normalizePhone(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 +127,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 +136,13 @@ public class UserService {
|
||||
user.setLastName(request.getLastName());
|
||||
user.setFullName(request.getFullName());
|
||||
user.setEmail(request.getEmail());
|
||||
String phone = trimToNull(request.getPhone());
|
||||
String phone = StringUtils.normalizePhone(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 +280,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;
|
||||
}
|
||||
|
||||
@@ -3,17 +3,26 @@ 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
|
||||
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()) {
|
||||
throw new RuntimeException("No authenticated user found");
|
||||
throw new AuthenticationCredentialsNotFoundException("No authenticated user found");
|
||||
}
|
||||
return authentication;
|
||||
}
|
||||
@@ -23,24 +32,47 @@ 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() {
|
||||
return getAuthenticatedPrincipal().getUserId();
|
||||
}
|
||||
|
||||
public static User.Role getAuthenticatedRole() {
|
||||
return getAuthenticatedPrincipal().getRole();
|
||||
}
|
||||
|
||||
public static boolean isCustomer() {
|
||||
return getAuthenticatedPrincipal().getRole() == User.Role.CUSTOMER;
|
||||
}
|
||||
|
||||
public User getAuthenticatedUser() {
|
||||
return getAuthenticatedUser(userRepository);
|
||||
}
|
||||
|
||||
public Long getCustomerIdOrNull() {
|
||||
if (!isCustomer()) {
|
||||
return null;
|
||||
}
|
||||
return getAuthenticatedUser().getId();
|
||||
}
|
||||
|
||||
public Long getEffectiveCustomerId(Long requestedCustomerId) {
|
||||
return isCustomer() ? getAuthenticatedUser().getId() : requestedCustomerId;
|
||||
}
|
||||
|
||||
public static User getAuthenticatedUser(UserRepository userRepository) {
|
||||
Authentication authentication = getAuthentication();
|
||||
Object principal = authentication.getPrincipal();
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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 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() > maxSizeBytes) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
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;
|
||||
}
|
||||
|
||||
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)));
|
||||
}
|
||||
}
|
||||
@@ -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:}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user