use shared StringUtils.trimToNull

This commit is contained in:
2026-04-17 13:54:00 -06:00
parent b17ca4fbbd
commit 84b6bac819
20 changed files with 200 additions and 361 deletions

View File

@@ -14,8 +14,6 @@ import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
@@ -41,17 +39,10 @@ public class AdoptionController {
@RequestParam(required = false) Long storeId,
@RequestParam(required = false) String date,
Pageable pageable) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long effectiveCustomerId = customerId;
if (role != null && role.equals("CUSTOMER")) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
effectiveCustomerId = user.getId();
}
Long effectiveCustomerId = AuthenticationHelper.isCustomer()
? AuthenticationHelper.getAuthenticatedUser(userRepository).getId()
: customerId;
LocalDate adoptionDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null;
@@ -61,18 +52,7 @@ public class AdoptionController {
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<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 = AuthenticationHelper.getCustomerIdOrNull(userRepository);
return ResponseEntity.ok(adoptionService.getAdoptionById(id, customerId));
}
@@ -94,18 +74,7 @@ public class AdoptionController {
@PatchMapping("/{id}/cancel")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<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 = AuthenticationHelper.getCustomerIdOrNull(userRepository);
return ResponseEntity.ok(adoptionService.cancelAdoption(id, customerId));
}

View File

@@ -13,8 +13,6 @@ import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
@@ -43,17 +41,9 @@ public class AppointmentController {
@RequestParam(required = false) Long employeeId,
Pageable pageable) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String role = authentication.getAuthorities().stream()
.findFirst()
.map(authority -> authority.getAuthority().replace("ROLE_", ""))
.orElse(null);
Long effectiveCustomerId = customerId;
if ("CUSTOMER".equals(role)) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
effectiveCustomerId = user.getId();
}
Long effectiveCustomerId = AuthenticationHelper.isCustomer()
? AuthenticationHelper.getAuthenticatedUser(userRepository).getId()
: customerId;
LocalDate appointmentDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null;
@@ -64,31 +54,14 @@ public class AppointmentController {
@GetMapping("/{id}")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<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 = AuthenticationHelper.getCustomerIdOrNull(userRepository);
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)) {
if (AuthenticationHelper.isCustomer()) {
User user = AuthenticationHelper.getAuthenticatedUser(userRepository);
if (!request.getCustomerId().equals(user.getId())) {
throw new org.springframework.security.access.AccessDeniedException("You can only create appointments for yourself");
@@ -101,18 +74,7 @@ public class AppointmentController {
@PatchMapping("/{id}/cancel")
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
public ResponseEntity<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 = AuthenticationHelper.getCustomerIdOrNull(userRepository);
return ResponseEntity.ok(appointmentService.cancelAppointment(id, customerId));
}

View File

@@ -25,6 +25,7 @@ import com.petshop.backend.service.EmailService;
import com.petshop.backend.service.PasswordResetService;
import com.petshop.backend.util.AuthenticationHelper;
import com.petshop.backend.util.PhoneUtils;
import com.petshop.backend.util.StringUtils;
import jakarta.validation.Valid;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpStatus;
@@ -78,10 +79,10 @@ public class AuthController {
@PostMapping("/register")
public ResponseEntity<RegisterResponse> register(@Valid @RequestBody RegisterRequest request) {
String username = trimToNull(request.getUsername());
String email = trimToNull(request.getEmail());
String firstName = trimToNull(request.getFirstName());
String lastName = trimToNull(request.getLastName());
String username = StringUtils.trimToNull(request.getUsername());
String email = StringUtils.trimToNull(request.getEmail());
String firstName = StringUtils.trimToNull(request.getFirstName());
String lastName = StringUtils.trimToNull(request.getLastName());
String phone = normalizePhone(request.getPhone());
if (userRepository.findByUsername(username).isPresent()) {
@@ -191,7 +192,7 @@ public class AuthController {
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
boolean invalidateToken = false;
String username = trimToNull(request.getUsername());
String username = StringUtils.trimToNull(request.getUsername());
if (username != null && !username.equals(user.getUsername())) {
if (userRepository.findByUsername(username).isPresent()) {
throw new ConflictException("Username already exists");
@@ -200,7 +201,7 @@ public class AuthController {
invalidateToken = true;
}
String email = trimToNull(request.getEmail());
String email = StringUtils.trimToNull(request.getEmail());
if (email != null && !email.equals(user.getEmail())) {
if (userRepository.findByEmail(email).isPresent()) {
throw new ConflictException("Email already exists");
@@ -208,11 +209,11 @@ public class AuthController {
user.setEmail(email);
}
String firstName = trimToNull(request.getFirstName());
String firstName = StringUtils.trimToNull(request.getFirstName());
if (firstName != null) {
user.setFirstName(firstName);
}
String lastName = trimToNull(request.getLastName());
String lastName = StringUtils.trimToNull(request.getLastName());
if (lastName != null) {
user.setLastName(lastName);
}
@@ -275,21 +276,13 @@ public class AuthController {
);
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private String normalizePhone(String value) {
return trimToNull(PhoneUtils.normalize(trimToNull(value)));
return StringUtils.trimToNull(PhoneUtils.normalize(StringUtils.trimToNull(value)));
}
private String joinFullName(String firstName, String lastName) {
String first = trimToNull(firstName);
String last = trimToNull(lastName);
String first = StringUtils.trimToNull(firstName);
String last = StringUtils.trimToNull(lastName);
if (first == null) {
return last == null ? null : last;
}

View File

@@ -3,7 +3,6 @@ package com.petshop.backend.controller;
import com.petshop.backend.dto.refund.RefundRequest;
import com.petshop.backend.dto.refund.RefundResponse;
import com.petshop.backend.dto.refund.RefundUpdateRequest;
import com.petshop.backend.entity.User;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.service.RefundService;
import com.petshop.backend.util.AuthenticationHelper;
@@ -11,8 +10,6 @@ import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -32,36 +29,14 @@ public class RefundController {
@PostMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF')")
public ResponseEntity<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 = AuthenticationHelper.getCustomerIdOrNull(userRepository);
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 = AuthenticationHelper.getCustomerIdOrNull(userRepository);
List<RefundResponse> refunds = refundService.getAllRefunds(customerId);
return ResponseEntity.ok(refunds);
}
@@ -69,18 +44,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 = AuthenticationHelper.getCustomerIdOrNull(userRepository);
return ResponseEntity.ok(refundService.getRefundById(id, customerId));
}

View File

@@ -1,6 +1,7 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.adoption.AdoptionRequest;
import com.petshop.backend.util.StringUtils;
import com.petshop.backend.dto.adoption.AdoptionResponse;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.entity.Adoption;
@@ -56,8 +57,8 @@ public class AdoptionService {
}
public Page<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,
@@ -248,14 +249,6 @@ public class AdoptionService {
}
}
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private AdoptionResponse mapToResponse(Adoption adoption) {
StoreLocation sourceStore = adoption.getSourceStore();
return new AdoptionResponse(

View File

@@ -20,6 +20,7 @@ import com.petshop.backend.repository.ServiceRepository;
import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.util.AuthenticationHelper;
import com.petshop.backend.util.StringUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -67,8 +68,8 @@ public class AppointmentService {
LocalDate date,
Pageable pageable) {
String normalizedQuery = normalizeFilter(query);
String normalizedStatus = normalizeFilter(status);
String normalizedQuery = StringUtils.trimToNull(query);
String normalizedStatus = StringUtils.trimToNull(status);
Page<Appointment> appointments = appointmentRepository.searchAppointments(
normalizedQuery,
@@ -288,14 +289,6 @@ public class AppointmentService {
}
}
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private void validatePetServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) {
if (pet == null) return;
Set<String> allowed = service.getSpecies();

View File

@@ -32,6 +32,7 @@ public class CartService {
private final StoreRepository storeRepository;
private final ProductRepository productRepository;
private final CouponRepository couponRepository;
private final CouponService couponService;
private final SaleRepository saleRepository;
private final SaleService saleService;
@@ -44,6 +45,7 @@ public class CartService {
StoreRepository storeRepository,
ProductRepository productRepository,
CouponRepository couponRepository,
CouponService couponService,
SaleRepository saleRepository,
SaleService saleService) {
this.cartRepository = cartRepository;
@@ -52,6 +54,7 @@ public class CartService {
this.storeRepository = storeRepository;
this.productRepository = productRepository;
this.couponRepository = couponRepository;
this.couponService = couponService;
this.saleRepository = saleRepository;
this.saleService = saleService;
}
@@ -188,31 +191,12 @@ public class CartService {
Coupon coupon = couponRepository.findByCouponCodeIgnoreCase(couponCode)
.orElseThrow(() -> new BusinessException("Invalid coupon code"));
LocalDateTime now = LocalDateTime.now();
if (!coupon.getActive()) {
throw new BusinessException("Coupon is no longer active");
}
if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) {
throw new BusinessException("Coupon is not yet valid");
}
if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) {
throw new BusinessException("Coupon has expired");
}
couponService.validateCouponForUse(coupon);
if (coupon.getMinOrderAmount() != null && cart.getSubtotalAmount().compareTo(coupon.getMinOrderAmount()) < 0) {
throw new BusinessException("Minimum order amount of $" + coupon.getMinOrderAmount() + " required");
}
if (coupon.getUsageLimit() != null) {
long used = saleRepository.countByCoupon_CouponId(coupon.getCouponId());
if (used >= coupon.getUsageLimit()) {
throw new BusinessException("Coupon usage limit has been reached");
}
}
cart.setCoupon(coupon);
recalculate(cart);
@@ -450,73 +434,38 @@ public class CartService {
}
}
private void recalculate(Cart cart) {
private record CartTotals(BigDecimal subtotal, BigDecimal discount, BigDecimal pointsDiscount, BigDecimal total) {}
private CartTotals computeTotals(Cart cart) {
List<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;

View File

@@ -1,6 +1,7 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.category.CategoryRequest;
import com.petshop.backend.util.StringUtils;
import com.petshop.backend.dto.category.CategoryResponse;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.entity.Category;
@@ -21,7 +22,7 @@ public class CategoryService {
}
public Page<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);
}
@@ -76,11 +77,4 @@ public class CategoryService {
);
}
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
}

View File

@@ -7,18 +7,25 @@ import com.petshop.backend.entity.Coupon;
import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.repository.CouponRepository;
import com.petshop.backend.repository.SaleRepository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
@Service
public class CouponService {
private final CouponRepository couponRepository;
private final SaleRepository saleRepository;
public CouponService(CouponRepository couponRepository) {
public CouponService(CouponRepository couponRepository, SaleRepository saleRepository) {
this.couponRepository = couponRepository;
this.saleRepository = saleRepository;
}
public Page<CouponResponse> getAllCoupons(String query, Boolean active, Pageable pageable) {
@@ -89,6 +96,53 @@ public class CouponService {
coupon.setUsageLimit(request.getUsageLimit());
}
public void validateCouponForUse(Coupon coupon) {
if (!Boolean.TRUE.equals(coupon.getActive())) {
throw new BusinessException("Coupon is no longer active");
}
LocalDateTime now = LocalDateTime.now();
if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) {
throw new BusinessException("Coupon is not yet valid");
}
if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) {
throw new BusinessException("Coupon has expired");
}
if (coupon.getUsageLimit() != null) {
long used = saleRepository.countByCoupon_CouponId(coupon.getCouponId());
if (used >= coupon.getUsageLimit()) {
throw new BusinessException("Coupon usage limit has been reached");
}
}
}
public BigDecimal calculateDiscount(Coupon coupon, BigDecimal subtotal) {
if (coupon == null || subtotal.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) {
return BigDecimal.ZERO;
}
BigDecimal discount = BigDecimal.ZERO;
String type = coupon.getDiscountType();
if (isPercentageType(type)) {
discount = subtotal.multiply(coupon.getDiscountValue())
.divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
} else if (isFixedType(type)) {
discount = coupon.getDiscountValue();
}
return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP);
}
public static boolean isPercentageType(String discountType) {
return "PERCENTAGE".equalsIgnoreCase(discountType) || "PERCENT".equalsIgnoreCase(discountType);
}
public static boolean isFixedType(String discountType) {
return "FIXED".equalsIgnoreCase(discountType) || "FLAT".equalsIgnoreCase(discountType);
}
private CouponResponse mapToResponse(Coupon coupon) {
return new CouponResponse(
coupon.getCouponId(),

View File

@@ -1,6 +1,7 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.util.StringUtils;
import com.petshop.backend.dto.inventory.InventoryRequest;
import com.petshop.backend.dto.inventory.InventoryResponse;
import com.petshop.backend.entity.Inventory;
@@ -29,7 +30,7 @@ public class InventoryService {
}
public Page<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);
}
@@ -93,14 +94,6 @@ public class InventoryService {
inventoryRepository.deleteAllById(request.getIds());
}
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private InventoryResponse mapToResponse(Inventory inventory) {
StoreLocation store = inventory.getStore();
return new InventoryResponse(

View File

@@ -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;
}
}

View File

@@ -1,6 +1,7 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.util.StringUtils;
import com.petshop.backend.dto.pet.MyPetRequest;
import com.petshop.backend.dto.pet.MyPetResponse;
import com.petshop.backend.dto.pet.PetRequest;
@@ -53,10 +54,10 @@ public class PetService {
@Transactional(readOnly = true)
public Page<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;
@@ -323,14 +324,6 @@ public class PetService {
return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status) || "owned".equalsIgnoreCase(status);
}
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private PetResponse mapToResponse(Pet pet) {
User owner = pet.getOwner();
StoreLocation store = pet.getStore();

View File

@@ -1,6 +1,7 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.common.BulkDeleteRequest;
import com.petshop.backend.util.StringUtils;
import com.petshop.backend.dto.product.ProductRequest;
import com.petshop.backend.dto.product.ProductResponse;
import com.petshop.backend.entity.Category;
@@ -38,7 +39,7 @@ public class ProductService {
}
public Page<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);
}
@@ -178,11 +179,4 @@ public class ProductService {
public record ImagePayload(Resource resource, MediaType mediaType) {
}
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
}

View File

@@ -1,6 +1,7 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.productsupplier.BulkDeleteProductSupplierRequest;
import com.petshop.backend.util.StringUtils;
import com.petshop.backend.dto.productsupplier.ProductSupplierRequest;
import com.petshop.backend.dto.productsupplier.ProductSupplierResponse;
import com.petshop.backend.entity.Product;
@@ -34,7 +35,7 @@ public class ProductSupplierService {
}
public Page<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);
@@ -97,14 +98,6 @@ public class ProductSupplierService {
});
}
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private Pageable mapSortProperties(Pageable pageable) {
if (pageable.getSort().isUnsorted()) {
return pageable;

View File

@@ -1,6 +1,7 @@
package com.petshop.backend.service;
import com.petshop.backend.dto.purchaseorder.PurchaseOrderResponse;
import com.petshop.backend.util.StringUtils;
import com.petshop.backend.entity.PurchaseOrder;
import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.exception.ResourceNotFoundException;
@@ -19,7 +20,7 @@ public class PurchaseOrderService {
}
public Page<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);
}
@@ -30,14 +31,6 @@ public class PurchaseOrderService {
return mapToResponse(purchaseOrder);
}
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private PurchaseOrderResponse mapToResponse(PurchaseOrder purchaseOrder) {
StoreLocation store = purchaseOrder.getStore();
return new PurchaseOrderResponse(

View File

@@ -8,6 +8,7 @@ import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.event.SaleReceiptEvent;
import com.petshop.backend.repository.*;
import com.petshop.backend.util.AuthenticationHelper;
import com.petshop.backend.util.StringUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
@@ -32,23 +33,25 @@ public class SaleService {
private final InventoryRepository inventoryRepository;
private final UserRepository userRepository;
private final CouponRepository couponRepository;
private final CouponService couponService;
private final CartRepository cartRepository;
private final ApplicationEventPublisher eventPublisher;
public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CartRepository cartRepository, ApplicationEventPublisher eventPublisher) {
public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CouponService couponService, CartRepository cartRepository, ApplicationEventPublisher eventPublisher) {
this.saleRepository = saleRepository;
this.productRepository = productRepository;
this.storeRepository = storeRepository;
this.inventoryRepository = inventoryRepository;
this.userRepository = userRepository;
this.couponRepository = couponRepository;
this.couponService = couponService;
this.cartRepository = cartRepository;
this.eventPublisher = eventPublisher;
}
@Transactional(readOnly = true)
public Page<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));
@@ -282,45 +285,6 @@ public class SaleService {
return mapToResponse(savedSale);
}
private void validateCoupon(Coupon coupon) {
if (!Boolean.TRUE.equals(coupon.getActive())) {
throw new BusinessException("Coupon is not active");
}
LocalDateTime now = LocalDateTime.now();
if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) {
throw new BusinessException("Coupon has not started yet");
}
if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) {
throw new BusinessException("Coupon has expired");
}
if (coupon.getUsageLimit() != null) {
long used = saleRepository.countByCoupon_CouponId(coupon.getCouponId());
if (used >= coupon.getUsageLimit()) {
throw new BusinessException("Coupon usage limit has been reached");
}
}
}
private BigDecimal calculateCouponDiscount(Coupon coupon, BigDecimal subtotal) {
if (coupon == null || subtotal.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) {
return BigDecimal.ZERO;
}
BigDecimal discount = BigDecimal.ZERO;
String type = coupon.getDiscountType().trim().toUpperCase();
if ("PERCENTAGE".equals(type) || "PERCENT".equals(type)) {
discount = subtotal.multiply(coupon.getDiscountValue().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP));
} else if ("FIXED".equals(type)) {
discount = coupon.getDiscountValue();
}
return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP);
}
private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) {
if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
@@ -419,14 +383,6 @@ public class SaleService {
return response;
}
private String normalizeFilter(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
String normalizePaymentMethod(String paymentMethod) {
if (paymentMethod == null) {
return null;

View File

@@ -10,6 +10,7 @@ import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.security.UserAuthCacheService;
import com.petshop.backend.util.AuthenticationHelper;
import com.petshop.backend.util.StringUtils;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.AccessDeniedException;
@@ -81,7 +82,7 @@ public class UserService {
requireRequestedRole(request.getRole(), requiredRole);
User user = new User();
user.setUsername(trimToNull(request.getUsername()));
user.setUsername(StringUtils.trimToNull(request.getUsername()));
if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
user.setPassword(passwordEncoder.encode(request.getPassword()));
}
@@ -89,9 +90,9 @@ public class UserService {
user.setLastName(request.getLastName());
user.setFullName(request.getFullName());
user.setEmail(request.getEmail());
user.setPhone(trimToNull(request.getPhone()));
user.setPhone(StringUtils.trimToNull(request.getPhone()));
user.setRole(request.getRole());
user.setStaffRole(trimToNull(request.getStaffRole()));
user.setStaffRole(StringUtils.trimToNull(request.getStaffRole()));
user.setPrimaryStore(resolveStore(request.getPrimaryStoreId()));
user.setActive(request.getActive() != null ? request.getActive() : true);
if (request.getLoyaltyPoints() != null) {
@@ -124,7 +125,7 @@ public class UserService {
|| user.getRole() != request.getRole()
|| !user.getActive().equals(request.getActive() != null ? request.getActive() : true);
user.setUsername(trimToNull(request.getUsername()));
user.setUsername(StringUtils.trimToNull(request.getUsername()));
if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
user.setPassword(passwordEncoder.encode(request.getPassword()));
invalidateToken = true;
@@ -133,13 +134,13 @@ public class UserService {
user.setLastName(request.getLastName());
user.setFullName(request.getFullName());
user.setEmail(request.getEmail());
String phone = trimToNull(request.getPhone());
String phone = StringUtils.trimToNull(request.getPhone());
if (!Objects.equals(user.getPhone(), phone)) {
validateUniquePhone(phone, user.getId());
}
user.setPhone(phone);
user.setRole(request.getRole());
user.setStaffRole(trimToNull(request.getStaffRole()));
user.setStaffRole(StringUtils.trimToNull(request.getStaffRole()));
user.setPrimaryStore(resolveStore(request.getPrimaryStoreId()));
user.setActive(request.getActive() != null ? request.getActive() : true);
if (request.getLoyaltyPoints() != null) {
@@ -277,16 +278,8 @@ public class UserService {
});
}
private String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
private User.Role parseRole(String role) {
String normalizedRole = trimToNull(role);
String normalizedRole = StringUtils.trimToNull(role);
if (normalizedRole == null) {
return null;
}

View File

@@ -33,6 +33,21 @@ public class AuthenticationHelper {
return getAuthenticatedPrincipal().getUserId();
}
public static User.Role getAuthenticatedRole() {
return getAuthenticatedPrincipal().getRole();
}
public static boolean isCustomer() {
return getAuthenticatedPrincipal().getRole() == User.Role.CUSTOMER;
}
public static Long getCustomerIdOrNull(UserRepository userRepository) {
if (!isCustomer()) {
return null;
}
return getAuthenticatedUser(userRepository).getId();
}
public static User getAuthenticatedUser(UserRepository userRepository) {
Authentication authentication = getAuthentication();
Object principal = authentication.getPrincipal();

View File

@@ -0,0 +1,30 @@
package com.petshop.backend.util;
import com.petshop.backend.exception.BusinessException;
import org.springframework.web.multipart.MultipartFile;
import java.util.Locale;
public final class ImageValidationUtil {
public static final long MAX_IMAGE_SIZE = 5 * 1024 * 1024;
private ImageValidationUtil() {}
public static void validate(MultipartFile file) {
if (file.isEmpty()) {
throw new BusinessException("Please select an image to upload");
}
if (file.getSize() > MAX_IMAGE_SIZE) {
throw new BusinessException("Image file size must be less than 5MB");
}
String contentType = file.getContentType();
if (contentType == null) {
throw new BusinessException("Only JPG, PNG, and GIF images are allowed");
}
String normalized = contentType.toLowerCase(Locale.ROOT);
if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) {
throw new BusinessException("Only JPG, PNG, and GIF images are allowed");
}
}
}

View File

@@ -0,0 +1,14 @@
package com.petshop.backend.util;
public final class StringUtils {
private StringUtils() {}
public static String trimToNull(String value) {
if (value == null) {
return null;
}
String trimmed = value.trim();
return trimmed.isEmpty() ? null : trimmed;
}
}