decouple emails from transactions

This commit is contained in:
2026-04-15 16:03:10 -06:00
parent 2031ecc99c
commit 33ef68f27a
9 changed files with 103 additions and 17 deletions

View File

@@ -0,0 +1,3 @@
package com.petshop.backend.event;
public record AdoptionConfirmedEvent(Long adoptionId) {}

View File

@@ -0,0 +1,3 @@
package com.petshop.backend.event;
public record AdoptionReminderEvent(Long adoptionId) {}

View File

@@ -0,0 +1,3 @@
package com.petshop.backend.event;
public record AppointmentConfirmedEvent(Long appointmentId) {}

View File

@@ -0,0 +1,3 @@
package com.petshop.backend.event;
public record AppointmentReminderEvent(Long appointmentId) {}

View File

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

View File

@@ -0,0 +1,3 @@
package com.petshop.backend.event;
public record SaleReceiptEvent(Long saleId) {}

View File

@@ -9,11 +9,14 @@ import com.petshop.backend.entity.Sale;
import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User; import com.petshop.backend.entity.User;
import com.petshop.backend.exception.ResourceNotFoundException; 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.AdoptionRepository;
import com.petshop.backend.repository.PetRepository; import com.petshop.backend.repository.PetRepository;
import com.petshop.backend.repository.SaleRepository; import com.petshop.backend.repository.SaleRepository;
import com.petshop.backend.repository.StoreRepository; import com.petshop.backend.repository.StoreRepository;
import com.petshop.backend.repository.UserRepository; import com.petshop.backend.repository.UserRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -40,15 +43,15 @@ public class AdoptionService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final StoreRepository storeRepository; private final StoreRepository storeRepository;
private final SaleRepository saleRepository; 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.adoptionRepository = adoptionRepository;
this.petRepository = petRepository; this.petRepository = petRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.storeRepository = storeRepository; this.storeRepository = storeRepository;
this.saleRepository = saleRepository; this.saleRepository = saleRepository;
this.emailService = emailService; this.eventPublisher = eventPublisher;
} }
public Page<AdoptionResponse> getAllAdoptions(String query, Long customerId, String status, Long storeId, LocalDate date, Pageable pageable) { public Page<AdoptionResponse> getAllAdoptions(String query, Long customerId, String status, Long storeId, LocalDate date, Pageable pageable) {
@@ -110,7 +113,7 @@ public class AdoptionService {
createSaleForAdoption(adoption, request.getPaymentMethod()); createSaleForAdoption(adoption, request.getPaymentMethod());
} }
if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus) || ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) { if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus) || ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) {
emailService.sendAdoptionConfirmation(adoption); eventPublisher.publishEvent(new AdoptionConfirmedEvent(adoption.getAdoptionId()));
} }
return mapToResponse(adoption); return mapToResponse(adoption);
} }
@@ -153,7 +156,7 @@ public class AdoptionService {
} }
boolean statusChanged = !adoptionStatus.equalsIgnoreCase(previousStatus); boolean statusChanged = !adoptionStatus.equalsIgnoreCase(previousStatus);
if (statusChanged && (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus) || ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus))) { if (statusChanged && (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus) || ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus))) {
emailService.sendAdoptionConfirmation(adoption); eventPublisher.publishEvent(new AdoptionConfirmedEvent(adoption.getAdoptionId()));
} }
return mapToResponse(adoption); return mapToResponse(adoption);
} }
@@ -343,7 +346,7 @@ public class AdoptionService {
sale.setIsRefund(false); sale.setIsRefund(false);
sale.setChannel("ADOPTION"); sale.setChannel("ADOPTION");
Sale savedSale = saleRepository.save(sale); 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) { private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId, User customer) {

View File

@@ -10,6 +10,9 @@ import com.petshop.backend.entity.StoreLocation;
import com.petshop.backend.entity.User; import com.petshop.backend.entity.User;
import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException; 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.AdoptionRepository;
import com.petshop.backend.repository.AppointmentRepository; import com.petshop.backend.repository.AppointmentRepository;
import com.petshop.backend.repository.PetRepository; 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.StoreRepository;
import com.petshop.backend.repository.UserRepository; import com.petshop.backend.repository.UserRepository;
import com.petshop.backend.util.AuthenticationHelper; import com.petshop.backend.util.AuthenticationHelper;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
@@ -41,16 +45,16 @@ public class AppointmentService {
private final StoreRepository storeRepository; private final StoreRepository storeRepository;
private final UserRepository userRepository; private final UserRepository userRepository;
private final AdoptionRepository adoptionRepository; 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.appointmentRepository = appointmentRepository;
this.serviceRepository = serviceRepository; this.serviceRepository = serviceRepository;
this.petRepository = petRepository; this.petRepository = petRepository;
this.storeRepository = storeRepository; this.storeRepository = storeRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.adoptionRepository = adoptionRepository; this.adoptionRepository = adoptionRepository;
this.emailService = emailService; this.eventPublisher = eventPublisher;
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -140,7 +144,7 @@ public class AppointmentService {
appointment.setPet(pet); appointment.setPet(pet);
appointment = appointmentRepository.save(appointment); appointment = appointmentRepository.save(appointment);
emailService.sendAppointmentConfirmation(appointment); eventPublisher.publishEvent(new AppointmentConfirmedEvent(appointment.getAppointmentId()));
return mapToResponse(appointment); return mapToResponse(appointment);
} }
@@ -179,7 +183,7 @@ public class AppointmentService {
appointment.setEmployee(employee); appointment.setEmployee(employee);
appointment = appointmentRepository.save(appointment); appointment = appointmentRepository.save(appointment);
emailService.sendAppointmentConfirmation(appointment); eventPublisher.publishEvent(new AppointmentConfirmedEvent(appointment.getAppointmentId()));
return mapToResponse(appointment); return mapToResponse(appointment);
} }
@@ -273,13 +277,13 @@ public class AppointmentService {
List<Appointment> tomorrowAppointments = appointmentRepository List<Appointment> tomorrowAppointments = appointmentRepository
.findByAppointmentDateAndAppointmentStatusIgnoreCase(tomorrow, "Booked"); .findByAppointmentDateAndAppointmentStatusIgnoreCase(tomorrow, "Booked");
for (Appointment appointment : tomorrowAppointments) { for (Appointment appointment : tomorrowAppointments) {
emailService.sendAppointmentReminder(appointment); eventPublisher.publishEvent(new AppointmentReminderEvent(appointment.getAppointmentId()));
} }
List<Adoption> tomorrowAdoptions = adoptionRepository List<Adoption> tomorrowAdoptions = adoptionRepository
.findByAdoptionDateAndAdoptionStatusIgnoreCase(tomorrow, "Pending"); .findByAdoptionDateAndAdoptionStatusIgnoreCase(tomorrow, "Pending");
for (Adoption adoption : tomorrowAdoptions) { for (Adoption adoption : tomorrowAdoptions) {
emailService.sendAdoptionReminder(adoption); eventPublisher.publishEvent(new AdoptionReminderEvent(adoption.getAdoptionId()));
} }
} }

View File

@@ -5,8 +5,10 @@ import com.petshop.backend.dto.sale.SaleResponse;
import com.petshop.backend.entity.*; import com.petshop.backend.entity.*;
import com.petshop.backend.exception.BusinessException; import com.petshop.backend.exception.BusinessException;
import com.petshop.backend.exception.ResourceNotFoundException; import com.petshop.backend.exception.ResourceNotFoundException;
import com.petshop.backend.event.SaleReceiptEvent;
import com.petshop.backend.repository.*; import com.petshop.backend.repository.*;
import com.petshop.backend.util.AuthenticationHelper; import com.petshop.backend.util.AuthenticationHelper;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -31,9 +33,9 @@ public class SaleService {
private final UserRepository userRepository; private final UserRepository userRepository;
private final CouponRepository couponRepository; private final CouponRepository couponRepository;
private final CartRepository cartRepository; 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.saleRepository = saleRepository;
this.productRepository = productRepository; this.productRepository = productRepository;
this.storeRepository = storeRepository; this.storeRepository = storeRepository;
@@ -41,7 +43,7 @@ public class SaleService {
this.userRepository = userRepository; this.userRepository = userRepository;
this.couponRepository = couponRepository; this.couponRepository = couponRepository;
this.cartRepository = cartRepository; this.cartRepository = cartRepository;
this.emailService = emailService; this.eventPublisher = eventPublisher;
} }
@Transactional(readOnly = true) @Transactional(readOnly = true)
@@ -272,7 +274,7 @@ public class SaleService {
Sale savedSale = saleRepository.save(sale); Sale savedSale = saleRepository.save(sale);
if (!Boolean.TRUE.equals(savedSale.getIsRefund()) && savedSale.getCustomer() != null) { if (!Boolean.TRUE.equals(savedSale.getIsRefund()) && savedSale.getCustomer() != null) {
emailService.sendPurchaseReceipt(savedSale); eventPublisher.publishEvent(new SaleReceiptEvent(savedSale.getSaleId()));
} }
return mapToResponse(savedSale); return mapToResponse(savedSale);