From 33ef68f27a6719ff9f139658da1fa03126d5094b Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 16:03:10 -0600 Subject: [PATCH] decouple emails from transactions --- .../backend/event/AdoptionConfirmedEvent.java | 3 + .../backend/event/AdoptionReminderEvent.java | 3 + .../event/AppointmentConfirmedEvent.java | 3 + .../event/AppointmentReminderEvent.java | 3 + .../backend/event/EmailEventListener.java | 62 +++++++++++++++++++ .../backend/event/SaleReceiptEvent.java | 3 + .../backend/service/AdoptionService.java | 15 +++-- .../backend/service/AppointmentService.java | 18 +++--- .../petshop/backend/service/SaleService.java | 10 +-- 9 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 backend/src/main/java/com/petshop/backend/event/AdoptionConfirmedEvent.java create mode 100644 backend/src/main/java/com/petshop/backend/event/AdoptionReminderEvent.java create mode 100644 backend/src/main/java/com/petshop/backend/event/AppointmentConfirmedEvent.java create mode 100644 backend/src/main/java/com/petshop/backend/event/AppointmentReminderEvent.java create mode 100644 backend/src/main/java/com/petshop/backend/event/EmailEventListener.java create mode 100644 backend/src/main/java/com/petshop/backend/event/SaleReceiptEvent.java 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 ed34bb91..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); } @@ -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 bb86d34c..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) @@ -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);