diff --git a/backend/src/main/java/com/petshop/backend/controller/ChatController.java b/backend/src/main/java/com/petshop/backend/controller/ChatController.java index e9d8459a..94fb229a 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ChatController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ChatController.java @@ -7,6 +7,7 @@ import com.petshop.backend.dto.chat.MessageResponse; import com.petshop.backend.dto.chat.UpdateConversationRequest; 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; @@ -115,7 +116,7 @@ public class ChatController { public ResponseEntity getMessageAttachment(@PathVariable Long messageId) { User user = getCurrentUser(); Message message = messageRepository.findById(messageId) - .orElseThrow(() -> new RuntimeException("Message not found")); + .orElseThrow(() -> new ResourceNotFoundException("Message not found with id: " + messageId)); if (!chatService.hasConversationAccess(message.getConversationId(), user.getId(), user.getRole())) { throw new AccessDeniedException("Access denied to this message attachment"); diff --git a/backend/src/main/java/com/petshop/backend/controller/ContactController.java b/backend/src/main/java/com/petshop/backend/controller/ContactController.java index c6b4cf1d..cfd4326f 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ContactController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ContactController.java @@ -1,6 +1,7 @@ package com.petshop.backend.controller; import com.petshop.backend.entity.User; +import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.repository.UserRepository; import com.petshop.backend.service.EmailService; import com.petshop.backend.util.AuthenticationHelper; @@ -33,7 +34,7 @@ public class ContactController { @PostMapping public ResponseEntity sendContactEmail(@Valid @RequestBody ContactRequest req) { Long userId = AuthenticationHelper.getAuthenticatedUserId(); - User user = userRepository.findById(userId).orElseThrow(); + User user = userRepository.findById(userId).orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + userId)); emailService.sendContactMessage(user, req.subject(), req.body()); return ResponseEntity.ok().build(); } diff --git a/backend/src/main/java/com/petshop/backend/controller/SaleController.java b/backend/src/main/java/com/petshop/backend/controller/SaleController.java index cdb64c78..dffa63e4 100644 --- a/backend/src/main/java/com/petshop/backend/controller/SaleController.java +++ b/backend/src/main/java/com/petshop/backend/controller/SaleController.java @@ -40,7 +40,7 @@ public class SaleController { } @PostMapping - @PreAuthorize("hasRole('STAFF')") + @PreAuthorize("hasAnyRole('STAFF', 'ADMIN')") public ResponseEntity createSale(@Valid @RequestBody SaleRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(saleService.createSale(request)); } diff --git a/backend/src/main/java/com/petshop/backend/controller/StoreController.java b/backend/src/main/java/com/petshop/backend/controller/StoreController.java index 7dfb2b01..dc7bf8f5 100644 --- a/backend/src/main/java/com/petshop/backend/controller/StoreController.java +++ b/backend/src/main/java/com/petshop/backend/controller/StoreController.java @@ -35,11 +35,13 @@ public class StoreController { } @PostMapping + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity createStore(@Valid @RequestBody StoreRequest request) { return ResponseEntity.status(HttpStatus.CREATED).body(storeService.createStore(request)); } @PutMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity updateStore( @PathVariable Long id, @Valid @RequestBody StoreRequest request) { @@ -47,12 +49,14 @@ public class StoreController { } @DeleteMapping("/{id}") + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity deleteStore(@PathVariable Long id) { storeService.deleteStore(id); return ResponseEntity.noContent().build(); } @DeleteMapping + @PreAuthorize("hasRole('ADMIN')") public ResponseEntity bulkDeleteStores(@Valid @RequestBody BulkDeleteRequest request) { storeService.bulkDeleteStores(request); return ResponseEntity.noContent().build(); diff --git a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java index 7fb9ceac..0a45996f 100644 --- a/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/adoption/AdoptionRequest.java @@ -20,6 +20,7 @@ public class AdoptionRequest { private Long employeeId; + @NotNull(message = "Source store ID is required") private Long sourceStoreId; private String paymentMethod; diff --git a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java index 9ddb9ad2..d1c3847a 100644 --- a/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/appointment/AppointmentRequest.java @@ -1,5 +1,6 @@ package com.petshop.backend.dto.appointment; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; import java.time.LocalTime; @@ -21,7 +22,7 @@ public class AppointmentRequest { @NotNull(message = "Appointment time is required") private LocalTime appointmentTime; - @NotNull(message = "Appointment status is required") + @NotBlank(message = "Appointment status is required") private String appointmentStatus; private Long petId; diff --git a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java index 9a92581a..ff4f9ad2 100644 --- a/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/pet/PetRequest.java @@ -100,7 +100,7 @@ public class PetRequest { Objects.equals(petSpecies, that.petSpecies) && Objects.equals(petBreed, that.petBreed) && Objects.equals(petAge, that.petAge) && - petStatus == that.petStatus && + Objects.equals(petStatus, that.petStatus) && Objects.equals(petPrice, that.petPrice); } diff --git a/backend/src/main/java/com/petshop/backend/dto/sale/SaleItemRequest.java b/backend/src/main/java/com/petshop/backend/dto/sale/SaleItemRequest.java index 94e094c3..dad7011a 100644 --- a/backend/src/main/java/com/petshop/backend/dto/sale/SaleItemRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/sale/SaleItemRequest.java @@ -9,6 +9,7 @@ public class SaleItemRequest { private Long prodId; @NotNull(message = "Quantity is required") + @Positive(message = "Quantity must be positive") private Integer quantity; public Long getProdId() { diff --git a/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java b/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java index c84ac5f7..977b72cc 100644 --- a/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java +++ b/backend/src/main/java/com/petshop/backend/dto/service/ServiceRequest.java @@ -18,6 +18,7 @@ public class ServiceRequest { @Positive(message = "Price must be positive") private BigDecimal servicePrice; + @NotNull(message = "Service duration is required") @Positive(message = "Duration must be positive") private Integer serviceDuration; diff --git a/backend/src/main/java/com/petshop/backend/event/AdoptionConfirmedEvent.java b/backend/src/main/java/com/petshop/backend/event/AdoptionConfirmedEvent.java new file mode 100644 index 00000000..87dad0ee --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/event/AdoptionConfirmedEvent.java @@ -0,0 +1,3 @@ +package com.petshop.backend.event; + +public record AdoptionConfirmedEvent(Long adoptionId) {} diff --git a/backend/src/main/java/com/petshop/backend/event/AdoptionReminderEvent.java b/backend/src/main/java/com/petshop/backend/event/AdoptionReminderEvent.java new file mode 100644 index 00000000..bfc3be52 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/event/AdoptionReminderEvent.java @@ -0,0 +1,3 @@ +package com.petshop.backend.event; + +public record AdoptionReminderEvent(Long adoptionId) {} diff --git a/backend/src/main/java/com/petshop/backend/event/AppointmentConfirmedEvent.java b/backend/src/main/java/com/petshop/backend/event/AppointmentConfirmedEvent.java new file mode 100644 index 00000000..9311e8dc --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/event/AppointmentConfirmedEvent.java @@ -0,0 +1,3 @@ +package com.petshop.backend.event; + +public record AppointmentConfirmedEvent(Long appointmentId) {} diff --git a/backend/src/main/java/com/petshop/backend/event/AppointmentReminderEvent.java b/backend/src/main/java/com/petshop/backend/event/AppointmentReminderEvent.java new file mode 100644 index 00000000..c8de75ce --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/event/AppointmentReminderEvent.java @@ -0,0 +1,3 @@ +package com.petshop.backend.event; + +public record AppointmentReminderEvent(Long appointmentId) {} diff --git a/backend/src/main/java/com/petshop/backend/event/EmailEventListener.java b/backend/src/main/java/com/petshop/backend/event/EmailEventListener.java new file mode 100644 index 00000000..71e0cbf6 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/event/EmailEventListener.java @@ -0,0 +1,62 @@ +package com.petshop.backend.event; + +import com.petshop.backend.repository.AdoptionRepository; +import com.petshop.backend.repository.AppointmentRepository; +import com.petshop.backend.repository.SaleRepository; +import com.petshop.backend.service.EmailService; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +public class EmailEventListener { + + private final AdoptionRepository adoptionRepository; + private final AppointmentRepository appointmentRepository; + private final SaleRepository saleRepository; + private final EmailService emailService; + + public EmailEventListener(AdoptionRepository adoptionRepository, AppointmentRepository appointmentRepository, SaleRepository saleRepository, EmailService emailService) { + this.adoptionRepository = adoptionRepository; + this.appointmentRepository = appointmentRepository; + this.saleRepository = saleRepository; + this.emailService = emailService; + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onAdoptionConfirmed(AdoptionConfirmedEvent event) { + adoptionRepository.findById(event.adoptionId()) + .ifPresent(emailService::sendAdoptionConfirmation); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onAdoptionReminder(AdoptionReminderEvent event) { + adoptionRepository.findById(event.adoptionId()) + .ifPresent(emailService::sendAdoptionReminder); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onAppointmentConfirmed(AppointmentConfirmedEvent event) { + appointmentRepository.findById(event.appointmentId()) + .ifPresent(emailService::sendAppointmentConfirmation); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onAppointmentReminder(AppointmentReminderEvent event) { + appointmentRepository.findById(event.appointmentId()) + .ifPresent(emailService::sendAppointmentReminder); + } + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void onSaleReceipt(SaleReceiptEvent event) { + saleRepository.findById(event.saleId()) + .ifPresent(emailService::sendPurchaseReceipt); + } +} diff --git a/backend/src/main/java/com/petshop/backend/event/SaleReceiptEvent.java b/backend/src/main/java/com/petshop/backend/event/SaleReceiptEvent.java new file mode 100644 index 00000000..d21df856 --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/event/SaleReceiptEvent.java @@ -0,0 +1,3 @@ +package com.petshop.backend.event; + +public record SaleReceiptEvent(Long saleId) {} 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 734f8c63..cc75e124 100644 --- a/backend/src/main/java/com/petshop/backend/service/AdoptionService.java +++ b/backend/src/main/java/com/petshop/backend/service/AdoptionService.java @@ -9,11 +9,14 @@ import com.petshop.backend.entity.Sale; import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.event.AdoptionConfirmedEvent; +import com.petshop.backend.event.SaleReceiptEvent; import com.petshop.backend.repository.AdoptionRepository; import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.SaleRepository; import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.UserRepository; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -40,15 +43,15 @@ public class AdoptionService { private final UserRepository userRepository; private final StoreRepository storeRepository; private final SaleRepository saleRepository; - private final EmailService emailService; + private final ApplicationEventPublisher eventPublisher; - public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, UserRepository userRepository, StoreRepository storeRepository, SaleRepository saleRepository, EmailService emailService) { + public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, UserRepository userRepository, StoreRepository storeRepository, SaleRepository saleRepository, ApplicationEventPublisher eventPublisher) { this.adoptionRepository = adoptionRepository; this.petRepository = petRepository; this.userRepository = userRepository; this.storeRepository = storeRepository; this.saleRepository = saleRepository; - this.emailService = emailService; + this.eventPublisher = eventPublisher; } public Page getAllAdoptions(String query, Long customerId, String status, Long storeId, LocalDate date, Pageable pageable) { @@ -110,7 +113,7 @@ public class AdoptionService { createSaleForAdoption(adoption, request.getPaymentMethod()); } if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus) || ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) { - emailService.sendAdoptionConfirmation(adoption); + eventPublisher.publishEvent(new AdoptionConfirmedEvent(adoption.getAdoptionId())); } return mapToResponse(adoption); } @@ -153,7 +156,7 @@ public class AdoptionService { } boolean statusChanged = !adoptionStatus.equalsIgnoreCase(previousStatus); if (statusChanged && (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus) || ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus))) { - emailService.sendAdoptionConfirmation(adoption); + eventPublisher.publishEvent(new AdoptionConfirmedEvent(adoption.getAdoptionId())); } return mapToResponse(adoption); } @@ -343,7 +346,7 @@ public class AdoptionService { sale.setIsRefund(false); sale.setChannel("ADOPTION"); Sale savedSale = saleRepository.save(sale); - emailService.sendPurchaseReceipt(savedSale); + eventPublisher.publishEvent(new SaleReceiptEvent(savedSale.getSaleId())); } private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId, User customer) { 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 bea0c682..999568a8 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -10,6 +10,9 @@ 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.AdoptionReminderEvent; +import com.petshop.backend.event.AppointmentConfirmedEvent; +import com.petshop.backend.event.AppointmentReminderEvent; import com.petshop.backend.repository.AdoptionRepository; import com.petshop.backend.repository.AppointmentRepository; import com.petshop.backend.repository.PetRepository; @@ -17,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 org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.scheduling.annotation.Scheduled; @@ -41,16 +45,16 @@ public class AppointmentService { private final StoreRepository storeRepository; private final UserRepository userRepository; private final AdoptionRepository adoptionRepository; - private final EmailService emailService; + private final ApplicationEventPublisher eventPublisher; - public AppointmentService(AppointmentRepository appointmentRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, AdoptionRepository adoptionRepository, EmailService emailService) { + public AppointmentService(AppointmentRepository appointmentRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, AdoptionRepository adoptionRepository, ApplicationEventPublisher eventPublisher) { this.appointmentRepository = appointmentRepository; this.serviceRepository = serviceRepository; this.petRepository = petRepository; this.storeRepository = storeRepository; this.userRepository = userRepository; this.adoptionRepository = adoptionRepository; - this.emailService = emailService; + this.eventPublisher = eventPublisher; } @Transactional(readOnly = true) @@ -140,7 +144,7 @@ public class AppointmentService { appointment.setPet(pet); appointment = appointmentRepository.save(appointment); - emailService.sendAppointmentConfirmation(appointment); + eventPublisher.publishEvent(new AppointmentConfirmedEvent(appointment.getAppointmentId())); return mapToResponse(appointment); } @@ -179,7 +183,7 @@ public class AppointmentService { appointment.setEmployee(employee); appointment = appointmentRepository.save(appointment); - emailService.sendAppointmentConfirmation(appointment); + eventPublisher.publishEvent(new AppointmentConfirmedEvent(appointment.getAppointmentId())); return mapToResponse(appointment); } @@ -264,7 +268,7 @@ public class AppointmentService { List pastBookedAppointments = appointmentRepository.findPastBookedAppointments(currentDate, currentTime); for (Appointment appointment : pastBookedAppointments) { - appointment.setAppointmentStatus("COMPLETED"); + appointment.setAppointmentStatus("Completed"); appointmentRepository.save(appointment); } @@ -273,13 +277,13 @@ public class AppointmentService { List tomorrowAppointments = appointmentRepository .findByAppointmentDateAndAppointmentStatusIgnoreCase(tomorrow, "Booked"); for (Appointment appointment : tomorrowAppointments) { - emailService.sendAppointmentReminder(appointment); + eventPublisher.publishEvent(new AppointmentReminderEvent(appointment.getAppointmentId())); } List tomorrowAdoptions = adoptionRepository .findByAdoptionDateAndAdoptionStatusIgnoreCase(tomorrow, "Pending"); for (Adoption adoption : tomorrowAdoptions) { - emailService.sendAdoptionReminder(adoption); + eventPublisher.publishEvent(new AdoptionReminderEvent(adoption.getAdoptionId())); } } 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 b53a4c4c..3428f6ac 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -5,8 +5,10 @@ import com.petshop.backend.dto.sale.SaleResponse; import com.petshop.backend.entity.*; import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.ResourceNotFoundException; +import com.petshop.backend.event.SaleReceiptEvent; import com.petshop.backend.repository.*; import com.petshop.backend.util.AuthenticationHelper; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; @@ -31,9 +33,9 @@ public class SaleService { private final UserRepository userRepository; private final CouponRepository couponRepository; private final CartRepository cartRepository; - private final EmailService emailService; + private final ApplicationEventPublisher eventPublisher; - public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CartRepository cartRepository, EmailService emailService) { + public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CartRepository cartRepository, ApplicationEventPublisher eventPublisher) { this.saleRepository = saleRepository; this.productRepository = productRepository; this.storeRepository = storeRepository; @@ -41,7 +43,7 @@ public class SaleService { this.userRepository = userRepository; this.couponRepository = couponRepository; this.cartRepository = cartRepository; - this.emailService = emailService; + this.eventPublisher = eventPublisher; } @Transactional(readOnly = true) @@ -172,7 +174,7 @@ public class SaleService { BigDecimal refundTotal; if (originalSubtotal != null && originalSubtotal.compareTo(BigDecimal.ZERO) > 0) { - BigDecimal ratio = subtotalAmount.divide(originalSubtotal, 10, RoundingMode.HALF_UP); + BigDecimal ratio = subtotalAmount.abs().divide(originalSubtotal, 10, RoundingMode.HALF_UP); refundTotal = originalSale.getTotalAmount().abs().multiply(ratio).negate().setScale(2, RoundingMode.HALF_UP); if (originalSale.getLoyaltyDiscountAmount() != null) { loyaltyDiscountRefunded = originalSale.getLoyaltyDiscountAmount().multiply(ratio).setScale(2, RoundingMode.HALF_UP); @@ -272,7 +274,7 @@ public class SaleService { Sale savedSale = saleRepository.save(sale); if (!Boolean.TRUE.equals(savedSale.getIsRefund()) && savedSale.getCustomer() != null) { - emailService.sendPurchaseReceipt(savedSale); + eventPublisher.publishEvent(new SaleReceiptEvent(savedSale.getSaleId())); } return mapToResponse(savedSale);