diff --git a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java index ee577f34..84005778 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AdoptionController.java @@ -14,8 +14,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; @@ -41,17 +39,10 @@ public class AdoptionController { @RequestParam(required = false) Long storeId, @RequestParam(required = false) String date, Pageable pageable) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - Long effectiveCustomerId = customerId; - if (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - effectiveCustomerId = user.getId(); - } + Long effectiveCustomerId = AuthenticationHelper.isCustomer() + ? AuthenticationHelper.getAuthenticatedUser(userRepository).getId() + : customerId; LocalDate adoptionDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null; @@ -61,18 +52,7 @@ public class AdoptionController { @GetMapping("/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAdoptionById(@PathVariable Long id) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - Long customerId = null; - if (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.ok(adoptionService.getAdoptionById(id, customerId)); } @@ -94,18 +74,7 @@ public class AdoptionController { @PatchMapping("/{id}/cancel") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity cancelAdoption(@PathVariable Long id) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - Long customerId = null; - if ("CUSTOMER".equals(role)) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.ok(adoptionService.cancelAdoption(id, customerId)); } diff --git a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java index e0214fa7..364f836e 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AppointmentController.java @@ -13,8 +13,6 @@ import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.time.LocalDate; @@ -43,17 +41,9 @@ public class AppointmentController { @RequestParam(required = false) Long employeeId, Pageable pageable) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - Long effectiveCustomerId = customerId; - if ("CUSTOMER".equals(role)) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - effectiveCustomerId = user.getId(); - } + Long effectiveCustomerId = AuthenticationHelper.isCustomer() + ? AuthenticationHelper.getAuthenticatedUser(userRepository).getId() + : customerId; LocalDate appointmentDate = (date != null && !date.isBlank()) ? LocalDate.parse(date) : null; @@ -64,31 +54,14 @@ public class AppointmentController { @GetMapping("/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getAppointmentById(@PathVariable Long id) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - Long customerId = null; - if (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.ok(appointmentService.getAppointmentById(id, customerId)); } @PostMapping @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity createAppointment(@Valid @RequestBody AppointmentRequest request) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - if ("CUSTOMER".equals(role)) { + if (AuthenticationHelper.isCustomer()) { User user = AuthenticationHelper.getAuthenticatedUser(userRepository); if (!request.getCustomerId().equals(user.getId())) { throw new org.springframework.security.access.AccessDeniedException("You can only create appointments for yourself"); @@ -101,18 +74,7 @@ public class AppointmentController { @PatchMapping("/{id}/cancel") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity cancelAppointment(@PathVariable Long id) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - Long customerId = null; - if ("CUSTOMER".equals(role)) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.ok(appointmentService.cancelAppointment(id, customerId)); } diff --git a/backend/src/main/java/com/petshop/backend/controller/AuthController.java b/backend/src/main/java/com/petshop/backend/controller/AuthController.java index 1b88551c..ad1e9be4 100644 --- a/backend/src/main/java/com/petshop/backend/controller/AuthController.java +++ b/backend/src/main/java/com/petshop/backend/controller/AuthController.java @@ -25,6 +25,7 @@ import com.petshop.backend.service.EmailService; import com.petshop.backend.service.PasswordResetService; import com.petshop.backend.util.AuthenticationHelper; import com.petshop.backend.util.PhoneUtils; +import com.petshop.backend.util.StringUtils; import jakarta.validation.Valid; import org.springframework.core.io.Resource; import org.springframework.http.HttpStatus; @@ -78,10 +79,10 @@ public class AuthController { @PostMapping("/register") public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { - String username = trimToNull(request.getUsername()); - String email = trimToNull(request.getEmail()); - String firstName = trimToNull(request.getFirstName()); - String lastName = trimToNull(request.getLastName()); + String username = StringUtils.trimToNull(request.getUsername()); + String email = StringUtils.trimToNull(request.getEmail()); + String firstName = StringUtils.trimToNull(request.getFirstName()); + String lastName = StringUtils.trimToNull(request.getLastName()); String phone = normalizePhone(request.getPhone()); if (userRepository.findByUsername(username).isPresent()) { @@ -191,7 +192,7 @@ public class AuthController { .orElseThrow(() -> new UsernameNotFoundException("User not found")); boolean invalidateToken = false; - String username = trimToNull(request.getUsername()); + String username = StringUtils.trimToNull(request.getUsername()); if (username != null && !username.equals(user.getUsername())) { if (userRepository.findByUsername(username).isPresent()) { throw new ConflictException("Username already exists"); @@ -200,7 +201,7 @@ public class AuthController { invalidateToken = true; } - String email = trimToNull(request.getEmail()); + String email = StringUtils.trimToNull(request.getEmail()); if (email != null && !email.equals(user.getEmail())) { if (userRepository.findByEmail(email).isPresent()) { throw new ConflictException("Email already exists"); @@ -208,11 +209,11 @@ public class AuthController { user.setEmail(email); } - String firstName = trimToNull(request.getFirstName()); + String firstName = StringUtils.trimToNull(request.getFirstName()); if (firstName != null) { user.setFirstName(firstName); } - String lastName = trimToNull(request.getLastName()); + String lastName = StringUtils.trimToNull(request.getLastName()); if (lastName != null) { user.setLastName(lastName); } @@ -275,21 +276,13 @@ public class AuthController { ); } - private String trimToNull(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private String normalizePhone(String value) { - return trimToNull(PhoneUtils.normalize(trimToNull(value))); + return StringUtils.trimToNull(PhoneUtils.normalize(StringUtils.trimToNull(value))); } private String joinFullName(String firstName, String lastName) { - String first = trimToNull(firstName); - String last = trimToNull(lastName); + String first = StringUtils.trimToNull(firstName); + String last = StringUtils.trimToNull(lastName); if (first == null) { return last == null ? null : last; } diff --git a/backend/src/main/java/com/petshop/backend/controller/RefundController.java b/backend/src/main/java/com/petshop/backend/controller/RefundController.java index 92fd22b3..da58c6a0 100644 --- a/backend/src/main/java/com/petshop/backend/controller/RefundController.java +++ b/backend/src/main/java/com/petshop/backend/controller/RefundController.java @@ -3,7 +3,6 @@ package com.petshop.backend.controller; import com.petshop.backend.dto.refund.RefundRequest; import com.petshop.backend.dto.refund.RefundResponse; import com.petshop.backend.dto.refund.RefundUpdateRequest; -import com.petshop.backend.entity.User; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.RefundService; import com.petshop.backend.util.AuthenticationHelper; @@ -11,8 +10,6 @@ import jakarta.validation.Valid; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -32,36 +29,14 @@ public class RefundController { @PostMapping @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF')") public ResponseEntity createRefund(@Valid @RequestBody RefundRequest request) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - Long customerId = null; - if (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.status(HttpStatus.CREATED).body(refundService.createRefund(request, customerId)); } @GetMapping @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity> getAllRefunds() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - Long customerId = null; - if (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); List refunds = refundService.getAllRefunds(customerId); return ResponseEntity.ok(refunds); } @@ -69,18 +44,7 @@ public class RefundController { @GetMapping("/{id}") @PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')") public ResponseEntity getRefundById(@PathVariable Long id) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - String role = authentication.getAuthorities().stream() - .findFirst() - .map(authority -> authority.getAuthority().replace("ROLE_", "")) - .orElse(null); - - Long customerId = null; - if (role != null && role.equals("CUSTOMER")) { - User user = AuthenticationHelper.getAuthenticatedUser(userRepository); - customerId = user.getId(); - } - + Long customerId = AuthenticationHelper.getCustomerIdOrNull(userRepository); return ResponseEntity.ok(refundService.getRefundById(id, customerId)); } diff --git a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java index 8af4a82c..679aa18c 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.adoption.AdoptionRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.adoption.AdoptionResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.Adoption; @@ -56,8 +57,8 @@ public class AdoptionService { } public Page getAllAdoptions(String query, Long customerId, String status, Long storeId, LocalDate date, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); - String normalizedStatus = normalizeFilter(status); + String normalizedQuery = StringUtils.trimToNull(query); + String normalizedStatus = StringUtils.trimToNull(status); Page adoptions = adoptionRepository.searchAdoptions( normalizedQuery, @@ -248,14 +249,6 @@ public class AdoptionService { } } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private AdoptionResponse mapToResponse(Adoption adoption) { StoreLocation sourceStore = adoption.getSourceStore(); return new AdoptionResponse( diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 1c0b2f1d..59f290a8 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -20,6 +20,7 @@ import com.petshop.backend.repository.ServiceRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.util.AuthenticationHelper; +import com.petshop.backend.util.StringUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -67,8 +68,8 @@ public class AppointmentService { LocalDate date, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); - String normalizedStatus = normalizeFilter(status); + String normalizedQuery = StringUtils.trimToNull(query); + String normalizedStatus = StringUtils.trimToNull(status); Page appointments = appointmentRepository.searchAppointments( normalizedQuery, @@ -288,14 +289,6 @@ public class AppointmentService { } } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private void validatePetServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) { if (pet == null) return; Set allowed = service.getSpecies(); diff --git a/backend/src/main/java/com/petshop/backend/service/CartService.java b/backend/src/main/java/com/petshop/backend/service/CartService.java index 200698f0..9e11ac13 100644 --- a/backend/src/main/java/com/petshop/backend/service/CartService.java +++ b/backend/src/main/java/com/petshop/backend/service/CartService.java @@ -32,6 +32,7 @@ public class CartService { private final StoreRepository storeRepository; private final ProductRepository productRepository; private final CouponRepository couponRepository; + private final CouponService couponService; private final SaleRepository saleRepository; private final SaleService saleService; @@ -44,6 +45,7 @@ public class CartService { StoreRepository storeRepository, ProductRepository productRepository, CouponRepository couponRepository, + CouponService couponService, SaleRepository saleRepository, SaleService saleService) { this.cartRepository = cartRepository; @@ -52,6 +54,7 @@ public class CartService { this.storeRepository = storeRepository; this.productRepository = productRepository; this.couponRepository = couponRepository; + this.couponService = couponService; this.saleRepository = saleRepository; this.saleService = saleService; } @@ -188,31 +191,12 @@ public class CartService { Coupon coupon = couponRepository.findByCouponCodeIgnoreCase(couponCode) .orElseThrow(() -> new BusinessException("Invalid coupon code")); - LocalDateTime now = LocalDateTime.now(); - - if (!coupon.getActive()) { - throw new BusinessException("Coupon is no longer active"); - } - - if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) { - throw new BusinessException("Coupon is not yet valid"); - } - - if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) { - throw new BusinessException("Coupon has expired"); - } + couponService.validateCouponForUse(coupon); if (coupon.getMinOrderAmount() != null && cart.getSubtotalAmount().compareTo(coupon.getMinOrderAmount()) < 0) { throw new BusinessException("Minimum order amount of $" + coupon.getMinOrderAmount() + " required"); } - if (coupon.getUsageLimit() != null) { - long used = saleRepository.countByCoupon_CouponId(coupon.getCouponId()); - if (used >= coupon.getUsageLimit()) { - throw new BusinessException("Coupon usage limit has been reached"); - } - } - cart.setCoupon(coupon); recalculate(cart); @@ -450,73 +434,38 @@ public class CartService { } } - private void recalculate(Cart cart) { + private record CartTotals(BigDecimal subtotal, BigDecimal discount, BigDecimal pointsDiscount, BigDecimal total) {} + + private CartTotals computeTotals(Cart cart) { List items = cartItemRepository.findByCartCartId(cart.getCartId()); BigDecimal subtotal = items.stream() .map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(i.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); - cart.setSubtotalAmount(subtotal); - - BigDecimal discount = BigDecimal.ZERO; - Coupon coupon = cart.getCoupon(); - - if (coupon != null) { - if (isPercentageType(coupon.getDiscountType())) { - discount = subtotal.multiply(coupon.getDiscountValue()) - .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); - } else if (isFixedType(coupon.getDiscountType())) { - discount = coupon.getDiscountValue().min(subtotal); - } - } - - discount = discount.max(BigDecimal.ZERO).min(subtotal); - cart.setDiscountAmount(discount); + BigDecimal discount = couponService.calculateDiscount(cart.getCoupon(), subtotal) + .max(BigDecimal.ZERO).min(subtotal); BigDecimal remainingAfterCoupon = subtotal.subtract(discount).max(BigDecimal.ZERO); BigDecimal pointsDiscount = calculatePointsDiscount(cart.getUser(), remainingAfterCoupon, Boolean.TRUE.equals(cart.getPointsApplied())); - cart.setPointsDiscountAmount(pointsDiscount); - cart.setTotalAmount(remainingAfterCoupon.subtract(pointsDiscount).max(BigDecimal.ZERO)); + BigDecimal total = remainingAfterCoupon.subtract(pointsDiscount).max(BigDecimal.ZERO); + + return new CartTotals(subtotal, discount, pointsDiscount, total); + } + + private void recalculate(Cart cart) { + CartTotals totals = computeTotals(cart); + cart.setSubtotalAmount(totals.subtotal()); + cart.setDiscountAmount(totals.discount()); + cart.setPointsDiscountAmount(totals.pointsDiscount()); + cart.setTotalAmount(totals.total()); cartRepository.save(cart); } private BigDecimal recalculateTotalAmount(Cart cart) { - List items = cartItemRepository.findByCartCartId(cart.getCartId()); - - BigDecimal subtotal = items.stream() - .map(i -> i.getUnitPrice().multiply(BigDecimal.valueOf(i.getQuantity()))) - .reduce(BigDecimal.ZERO, BigDecimal::add); - - BigDecimal discount = BigDecimal.ZERO; - Coupon coupon = cart.getCoupon(); - - if (coupon != null) { - if (isPercentageType(coupon.getDiscountType())) { - discount = subtotal.multiply(coupon.getDiscountValue()) - .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); - } else if (isFixedType(coupon.getDiscountType())) { - discount = coupon.getDiscountValue().min(subtotal); - } - } - - discount = discount.max(BigDecimal.ZERO).min(subtotal); - - BigDecimal remainingAfterCoupon = subtotal.subtract(discount).max(BigDecimal.ZERO); - BigDecimal pointsDiscount = calculatePointsDiscount(cart.getUser(), remainingAfterCoupon, Boolean.TRUE.equals(cart.getPointsApplied())); - - return remainingAfterCoupon.subtract(pointsDiscount).max(BigDecimal.ZERO); + return computeTotals(cart).total(); } - private boolean isPercentageType(String discountType) { - return "PERCENTAGE".equalsIgnoreCase(discountType) || "PERCENT".equalsIgnoreCase(discountType); - } - - private boolean isFixedType(String discountType) { - return "FIXED".equalsIgnoreCase(discountType) || "FLAT".equalsIgnoreCase(discountType); - } - - private BigDecimal calculatePointsDiscount(User user, BigDecimal remainingAmount, boolean pointsApplied) { if (!pointsApplied || user == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) { return BigDecimal.ZERO; diff --git a/backend/src/main/java/com/petshop/backend/service/CategoryService.java b/backend/src/main/java/com/petshop/backend/service/CategoryService.java index 1bf175c7..742ae847 100644 --- a/backend/src/main/java/com/petshop/backend/service/CategoryService.java +++ b/backend/src/main/java/com/petshop/backend/service/CategoryService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.category.CategoryRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.category.CategoryResponse; import com.petshop.backend.dto.common.BulkDeleteRequest; import com.petshop.backend.entity.Category; @@ -21,7 +22,7 @@ public class CategoryService { } public Page getAllCategories(String query, String type, Pageable pageable) { - return categoryRepository.searchCategories(normalizeFilter(query), normalizeFilter(type), pageable) + return categoryRepository.searchCategories(StringUtils.trimToNull(query), StringUtils.trimToNull(type), pageable) .map(this::mapToResponse); } @@ -76,11 +77,4 @@ public class CategoryService { ); } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } } diff --git a/backend/src/main/java/com/petshop/backend/service/CouponService.java b/backend/src/main/java/com/petshop/backend/service/CouponService.java index 89cac69a..eb3b605d 100644 --- a/backend/src/main/java/com/petshop/backend/service/CouponService.java +++ b/backend/src/main/java/com/petshop/backend/service/CouponService.java @@ -7,18 +7,25 @@ import com.petshop.backend.entity.Coupon; import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.CouponRepository; +import com.petshop.backend.repository.SaleRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDateTime; + @Service public class CouponService { private final CouponRepository couponRepository; + private final SaleRepository saleRepository; - public CouponService(CouponRepository couponRepository) { + public CouponService(CouponRepository couponRepository, SaleRepository saleRepository) { this.couponRepository = couponRepository; + this.saleRepository = saleRepository; } public Page getAllCoupons(String query, Boolean active, Pageable pageable) { @@ -89,6 +96,53 @@ public class CouponService { coupon.setUsageLimit(request.getUsageLimit()); } + public void validateCouponForUse(Coupon coupon) { + if (!Boolean.TRUE.equals(coupon.getActive())) { + throw new BusinessException("Coupon is no longer active"); + } + LocalDateTime now = LocalDateTime.now(); + if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) { + throw new BusinessException("Coupon is not yet valid"); + } + if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) { + throw new BusinessException("Coupon has expired"); + } + if (coupon.getUsageLimit() != null) { + long used = saleRepository.countByCoupon_CouponId(coupon.getCouponId()); + if (used >= coupon.getUsageLimit()) { + throw new BusinessException("Coupon usage limit has been reached"); + } + } + } + + public BigDecimal calculateDiscount(Coupon coupon, BigDecimal subtotal) { + if (coupon == null || subtotal.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) { + return BigDecimal.ZERO; + } + + BigDecimal discount = BigDecimal.ZERO; + String type = coupon.getDiscountType(); + if (isPercentageType(type)) { + discount = subtotal.multiply(coupon.getDiscountValue()) + .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP); + } else if (isFixedType(type)) { + discount = coupon.getDiscountValue(); + } + + return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP); + } + + public static boolean isPercentageType(String discountType) { + return "PERCENTAGE".equalsIgnoreCase(discountType) || "PERCENT".equalsIgnoreCase(discountType); + } + + public static boolean isFixedType(String discountType) { + return "FIXED".equalsIgnoreCase(discountType) || "FLAT".equalsIgnoreCase(discountType); + } + private CouponResponse mapToResponse(Coupon coupon) { return new CouponResponse( coupon.getCouponId(), diff --git a/backend/src/main/java/com/petshop/backend/service/InventoryService.java b/backend/src/main/java/com/petshop/backend/service/InventoryService.java index f7e15068..021ed021 100644 --- a/backend/src/main/java/com/petshop/backend/service/InventoryService.java +++ b/backend/src/main/java/com/petshop/backend/service/InventoryService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.inventory.InventoryRequest; import com.petshop.backend.dto.inventory.InventoryResponse; import com.petshop.backend.entity.Inventory; @@ -29,7 +30,7 @@ public class InventoryService { } public Page getAllInventory(String query, Long storeId, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); + String normalizedQuery = StringUtils.trimToNull(query); Page inventory = inventoryRepository.searchInventory(normalizedQuery, storeId, pageable); return inventory.map(this::mapToResponse); } @@ -93,14 +94,6 @@ public class InventoryService { inventoryRepository.deleteAllById(request.getIds()); } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private InventoryResponse mapToResponse(Inventory inventory) { StoreLocation store = inventory.getStore(); return new InventoryResponse( diff --git a/backend/src/main/java/com/petshop/backend/service/PasswordResetService.java b/backend/src/main/java/com/petshop/backend/service/PasswordResetService.java index f90cf6d5..6c5c78f5 100644 --- a/backend/src/main/java/com/petshop/backend/service/PasswordResetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PasswordResetService.java @@ -6,6 +6,7 @@ import com.petshop.backend.entity.PasswordResetToken; import com.petshop.backend.entity.User; import com.petshop.backend.exception.BusinessException; import com.petshop.backend.repository.PasswordResetTokenRepository; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.security.UserAuthCacheService; import org.springframework.security.crypto.password.PasswordEncoder; @@ -47,7 +48,7 @@ public class PasswordResetService { @Transactional public ForgotPasswordResponse createResetToken(String usernameOrEmail) { - String normalized = trimToNull(usernameOrEmail); + String normalized = StringUtils.trimToNull(usernameOrEmail); if (normalized == null) { throw new BusinessException("Username or email is required"); } @@ -83,7 +84,7 @@ public class PasswordResetService { @Transactional public ResetPasswordResponse resetPassword(String rawToken, String newPassword) { - String normalizedToken = trimToNull(rawToken); + String normalizedToken = StringUtils.trimToNull(rawToken); if (normalizedToken == null) { throw new BusinessException("Reset token is required"); } @@ -136,11 +137,4 @@ public class PasswordResetService { } } - private String trimToNull(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } } diff --git a/backend/src/main/java/com/petshop/backend/service/PetService.java b/backend/src/main/java/com/petshop/backend/service/PetService.java index f901b797..98b63cd8 100644 --- a/backend/src/main/java/com/petshop/backend/service/PetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PetService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.pet.MyPetRequest; import com.petshop.backend.dto.pet.MyPetResponse; import com.petshop.backend.dto.pet.PetRequest; @@ -53,10 +54,10 @@ public class PetService { @Transactional(readOnly = true) public Page getAllPets(String query, String species, String breed, String status, Long storeId, Long customerId, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); - String normalizedSpecies = normalizeFilter(species); - String normalizedBreed = normalizeFilter(breed); - String normalizedStatus = normalizeFilter(status); + String normalizedQuery = StringUtils.trimToNull(query); + String normalizedSpecies = StringUtils.trimToNull(species); + String normalizedBreed = StringUtils.trimToNull(breed); + String normalizedStatus = StringUtils.trimToNull(status); CurrentViewer viewer = getCurrentViewer(); Page pets; @@ -323,14 +324,6 @@ public class PetService { return status == null || "available".equalsIgnoreCase(status) || "adopted".equalsIgnoreCase(status) || "owned".equalsIgnoreCase(status); } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private PetResponse mapToResponse(Pet pet) { User owner = pet.getOwner(); StoreLocation store = pet.getStore(); diff --git a/backend/src/main/java/com/petshop/backend/service/ProductService.java b/backend/src/main/java/com/petshop/backend/service/ProductService.java index 5234ef22..9f2a9069 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.common.BulkDeleteRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.product.ProductRequest; import com.petshop.backend.dto.product.ProductResponse; import com.petshop.backend.entity.Category; @@ -38,7 +39,7 @@ public class ProductService { } public Page getAllProducts(String query, Long categoryId, Pageable pageable) { - return productRepository.searchProducts(normalizeFilter(query), categoryId, pageable) + return productRepository.searchProducts(StringUtils.trimToNull(query), categoryId, pageable) .map(this::mapToResponse); } @@ -178,11 +179,4 @@ public class ProductService { public record ImagePayload(Resource resource, MediaType mediaType) { } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } } diff --git a/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java index c9be85f1..0f6250d2 100644 --- a/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java +++ b/backend/src/main/java/com/petshop/backend/service/ProductSupplierService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.productsupplier.BulkDeleteProductSupplierRequest; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.dto.productsupplier.ProductSupplierRequest; import com.petshop.backend.dto.productsupplier.ProductSupplierResponse; import com.petshop.backend.entity.Product; @@ -34,7 +35,7 @@ public class ProductSupplierService { } public Page getAllProductSuppliers(String query, Long productId, Long supplierId, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); + String normalizedQuery = StringUtils.trimToNull(query); Pageable mappedPageable = mapSortProperties(pageable); Page productSuppliers = productSupplierRepository.searchProductSuppliers(normalizedQuery, productId, supplierId, mappedPageable); return productSuppliers.map(this::mapToResponse); @@ -97,14 +98,6 @@ public class ProductSupplierService { }); } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private Pageable mapSortProperties(Pageable pageable) { if (pageable.getSort().isUnsorted()) { return pageable; diff --git a/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java index f3099354..0f2a1d07 100644 --- a/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java +++ b/backend/src/main/java/com/petshop/backend/service/PurchaseOrderService.java @@ -1,6 +1,7 @@ package com.petshop.backend.service; import com.petshop.backend.dto.purchaseorder.PurchaseOrderResponse; +import com.petshop.backend.util.StringUtils; import com.petshop.backend.entity.PurchaseOrder; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.exception.ResourceNotFoundException; @@ -19,7 +20,7 @@ public class PurchaseOrderService { } public Page getAllPurchaseOrders(String query, Long storeId, Pageable pageable) { - String normalizedQuery = normalizeFilter(query); + String normalizedQuery = StringUtils.trimToNull(query); Page purchaseOrders = purchaseOrderRepository.searchPurchaseOrders(normalizedQuery, storeId, pageable); return purchaseOrders.map(this::mapToResponse); } @@ -30,14 +31,6 @@ public class PurchaseOrderService { return mapToResponse(purchaseOrder); } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private PurchaseOrderResponse mapToResponse(PurchaseOrder purchaseOrder) { StoreLocation store = purchaseOrder.getStore(); return new PurchaseOrderResponse( diff --git a/backend/src/main/java/com/petshop/backend/service/SaleService.java b/backend/src/main/java/com/petshop/backend/service/SaleService.java index d43919bc..f69ef9e1 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -8,6 +8,7 @@ import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.event.SaleReceiptEvent; import com.petshop.backend.repository.*; import com.petshop.backend.util.AuthenticationHelper; +import com.petshop.backend.util.StringUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -32,23 +33,25 @@ public class SaleService { private final InventoryRepository inventoryRepository; private final UserRepository userRepository; private final CouponRepository couponRepository; + private final CouponService couponService; private final CartRepository cartRepository; private final ApplicationEventPublisher eventPublisher; - public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CartRepository cartRepository, ApplicationEventPublisher eventPublisher) { + public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CouponService couponService, CartRepository cartRepository, ApplicationEventPublisher eventPublisher) { this.saleRepository = saleRepository; this.productRepository = productRepository; this.storeRepository = storeRepository; this.inventoryRepository = inventoryRepository; this.userRepository = userRepository; this.couponRepository = couponRepository; + this.couponService = couponService; this.cartRepository = cartRepository; this.eventPublisher = eventPublisher; } @Transactional(readOnly = true) public Page getAllSales(String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId, Pageable pageable) { - Page sales = saleRepository.searchSales(normalizeFilter(query), normalizeFilter(paymentMethod), storeId, isRefund, customerId, pageable); + Page sales = saleRepository.searchSales(StringUtils.trimToNull(query), StringUtils.trimToNull(paymentMethod), storeId, isRefund, customerId, pageable); return sales.map(this::mapToResponse); } @@ -86,7 +89,7 @@ public class SaleService { if (request.getCouponId() != null) { Coupon coupon = couponRepository.findByIdForUpdate(request.getCouponId()) .orElseThrow(() -> new ResourceNotFoundException("Coupon not found with id: " + request.getCouponId())); - validateCoupon(coupon); + couponService.validateCouponForUse(coupon); sale.setCoupon(coupon); } @@ -236,7 +239,7 @@ public class SaleService { } sale.setSubtotalAmount(subtotalAmount); - BigDecimal couponDiscount = calculateCouponDiscount(sale.getCoupon(), subtotalAmount); + BigDecimal couponDiscount = couponService.calculateDiscount(sale.getCoupon(), subtotalAmount); sale.setCouponDiscountAmount(couponDiscount); BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount)); @@ -282,45 +285,6 @@ public class SaleService { return mapToResponse(savedSale); } - private void validateCoupon(Coupon coupon) { - if (!Boolean.TRUE.equals(coupon.getActive())) { - throw new BusinessException("Coupon is not active"); - } - LocalDateTime now = LocalDateTime.now(); - if (coupon.getStartsAt() != null && now.isBefore(coupon.getStartsAt())) { - throw new BusinessException("Coupon has not started yet"); - } - if (coupon.getEndsAt() != null && now.isAfter(coupon.getEndsAt())) { - throw new BusinessException("Coupon has expired"); - } - if (coupon.getUsageLimit() != null) { - long used = saleRepository.countByCoupon_CouponId(coupon.getCouponId()); - if (used >= coupon.getUsageLimit()) { - throw new BusinessException("Coupon usage limit has been reached"); - } - } - } - - private BigDecimal calculateCouponDiscount(Coupon coupon, BigDecimal subtotal) { - if (coupon == null || subtotal.compareTo(BigDecimal.ZERO) <= 0) { - return BigDecimal.ZERO; - } - - if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) { - return BigDecimal.ZERO; - } - - BigDecimal discount = BigDecimal.ZERO; - String type = coupon.getDiscountType().trim().toUpperCase(); - if ("PERCENTAGE".equals(type) || "PERCENT".equals(type)) { - discount = subtotal.multiply(coupon.getDiscountValue().divide(new BigDecimal("100"), 4, RoundingMode.HALF_UP)); - } else if ("FIXED".equals(type)) { - discount = coupon.getDiscountValue(); - } - - return discount.min(subtotal).setScale(2, RoundingMode.HALF_UP); - } - private BigDecimal calculateEmployeeDiscount(User customer, BigDecimal remainingAmount) { if (customer == null || remainingAmount.compareTo(BigDecimal.ZERO) <= 0) { return BigDecimal.ZERO; @@ -419,14 +383,6 @@ public class SaleService { return response; } - private String normalizeFilter(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - String normalizePaymentMethod(String paymentMethod) { if (paymentMethod == null) { return null; diff --git a/backend/src/main/java/com/petshop/backend/service/UserService.java b/backend/src/main/java/com/petshop/backend/service/UserService.java index 75966114..4fe91a26 100644 --- a/backend/src/main/java/com/petshop/backend/service/UserService.java +++ b/backend/src/main/java/com/petshop/backend/service/UserService.java @@ -10,6 +10,7 @@ import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.security.UserAuthCacheService; import com.petshop.backend.util.AuthenticationHelper; +import com.petshop.backend.util.StringUtils; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.security.access.AccessDeniedException; @@ -81,7 +82,7 @@ public class UserService { requireRequestedRole(request.getRole(), requiredRole); User user = new User(); - user.setUsername(trimToNull(request.getUsername())); + user.setUsername(StringUtils.trimToNull(request.getUsername())); if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) { user.setPassword(passwordEncoder.encode(request.getPassword())); } @@ -89,9 +90,9 @@ public class UserService { user.setLastName(request.getLastName()); user.setFullName(request.getFullName()); user.setEmail(request.getEmail()); - user.setPhone(trimToNull(request.getPhone())); + user.setPhone(StringUtils.trimToNull(request.getPhone())); user.setRole(request.getRole()); - user.setStaffRole(trimToNull(request.getStaffRole())); + user.setStaffRole(StringUtils.trimToNull(request.getStaffRole())); user.setPrimaryStore(resolveStore(request.getPrimaryStoreId())); user.setActive(request.getActive() != null ? request.getActive() : true); if (request.getLoyaltyPoints() != null) { @@ -124,7 +125,7 @@ public class UserService { || user.getRole() != request.getRole() || !user.getActive().equals(request.getActive() != null ? request.getActive() : true); - user.setUsername(trimToNull(request.getUsername())); + user.setUsername(StringUtils.trimToNull(request.getUsername())); if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) { user.setPassword(passwordEncoder.encode(request.getPassword())); invalidateToken = true; @@ -133,13 +134,13 @@ public class UserService { user.setLastName(request.getLastName()); user.setFullName(request.getFullName()); user.setEmail(request.getEmail()); - String phone = trimToNull(request.getPhone()); + String phone = StringUtils.trimToNull(request.getPhone()); if (!Objects.equals(user.getPhone(), phone)) { validateUniquePhone(phone, user.getId()); } user.setPhone(phone); user.setRole(request.getRole()); - user.setStaffRole(trimToNull(request.getStaffRole())); + user.setStaffRole(StringUtils.trimToNull(request.getStaffRole())); user.setPrimaryStore(resolveStore(request.getPrimaryStoreId())); user.setActive(request.getActive() != null ? request.getActive() : true); if (request.getLoyaltyPoints() != null) { @@ -277,16 +278,8 @@ public class UserService { }); } - private String trimToNull(String value) { - if (value == null) { - return null; - } - String trimmed = value.trim(); - return trimmed.isEmpty() ? null : trimmed; - } - private User.Role parseRole(String role) { - String normalizedRole = trimToNull(role); + String normalizedRole = StringUtils.trimToNull(role); if (normalizedRole == null) { return null; } diff --git a/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java index c78e87d3..4586ab2d 100644 --- a/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java +++ b/backend/src/main/java/com/petshop/backend/util/AuthenticationHelper.java @@ -33,6 +33,21 @@ public class AuthenticationHelper { return getAuthenticatedPrincipal().getUserId(); } + public static User.Role getAuthenticatedRole() { + return getAuthenticatedPrincipal().getRole(); + } + + public static boolean isCustomer() { + return getAuthenticatedPrincipal().getRole() == User.Role.CUSTOMER; + } + + public static Long getCustomerIdOrNull(UserRepository userRepository) { + if (!isCustomer()) { + return null; + } + return getAuthenticatedUser(userRepository).getId(); + } + public static User getAuthenticatedUser(UserRepository userRepository) { Authentication authentication = getAuthentication(); Object principal = authentication.getPrincipal(); diff --git a/backend/src/main/java/com/petshop/backend/util/ImageValidationUtil.java b/backend/src/main/java/com/petshop/backend/util/ImageValidationUtil.java new file mode 100644 index 00000000..30214fb4 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/util/ImageValidationUtil.java @@ -0,0 +1,30 @@ +package com.petshop.backend.util; + +import com.petshop.backend.exception.BusinessException; +import org.springframework.web.multipart.MultipartFile; + +import java.util.Locale; + +public final class ImageValidationUtil { + + public static final long MAX_IMAGE_SIZE = 5 * 1024 * 1024; + + private ImageValidationUtil() {} + + public static void validate(MultipartFile file) { + if (file.isEmpty()) { + throw new BusinessException("Please select an image to upload"); + } + if (file.getSize() > MAX_IMAGE_SIZE) { + throw new BusinessException("Image file size must be less than 5MB"); + } + String contentType = file.getContentType(); + if (contentType == null) { + throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); + } + String normalized = contentType.toLowerCase(Locale.ROOT); + if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) { + throw new BusinessException("Only JPG, PNG, and GIF images are allowed"); + } + } +} diff --git a/backend/src/main/java/com/petshop/backend/util/StringUtils.java b/backend/src/main/java/com/petshop/backend/util/StringUtils.java new file mode 100644 index 00000000..625c294d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/util/StringUtils.java @@ -0,0 +1,14 @@ +package com.petshop.backend.util; + +public final class StringUtils { + + private StringUtils() {} + + public static String trimToNull(String value) { + if (value == null) { + return null; + } + String trimmed = value.trim(); + return trimmed.isEmpty() ? null : trimmed; + } +}