Your account has been created successfully.
+You can now log in and start exploring our pets, products, and services.
+Thank you for joining us!
+Hi %s,
+We received a request to reset your password. Click the button below to proceed.
+ +This link expires in 30 minutes. If you did not request a reset, you can safely ignore this email.
+Date: %s
+Payment method: %s
+| Item | +Qty | +Unit price | +Total | +
|---|
Subtotal: $%s
+ %s + %s + %s +Total: $%s
+ %s +You earned " + sale.getPointsEarned() + " loyalty points on this order.
" + : "" + ); + } + + private String buildAdoptionHtml(Adoption adoption, User customer) { + String petName = adoption.getPet() != null ? adoption.getPet().getPetName() : "—"; + String storeName = adoption.getSourceStore() != null ? adoption.getSourceStore().getStoreName() : "—"; + String date = adoption.getAdoptionDate() != null ? adoption.getAdoptionDate().format(DATE_FMT) : "—"; + String customerName = customer != null ? firstName(customer) : "—"; + return """ +Customer: %s
+Pet: %s
+Date: %s
+Store: %s
+Status: %s
+This is a reminder that the adoption of %s is scheduled for %s at %s.
+Service: %s
+Date: %s
+Time: %s
+Location: %s
+Staff: %s
+ %s +Pet: " + esc(pet) + "
" : ""); + } + + private String buildAppointmentReminderHtml(Appointment appointment) { + String service = appointment.getService() != null ? appointment.getService().getServiceName() : "—"; + String store = appointment.getStore() != null ? appointment.getStore().getStoreName() : "—"; + String time = appointment.getAppointmentTime() != null ? appointment.getAppointmentTime().format(TIME_FMT) : "—"; + return """ +Your %s appointment is scheduled for tomorrow at %s at %s.
+Hi %s, here is the transcript of your support conversation.
+| Time | +Sender | +Message | +
|---|
" + label + ": -$" + fmt(value) + "
"; + } + + private String esc(String s) { + if (s == null) return ""; + return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """); + } +} 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 77a14d18..5094f613 100644 --- a/backend/src/main/java/com/petshop/backend/service/PasswordResetService.java +++ b/backend/src/main/java/com/petshop/backend/service/PasswordResetService.java @@ -28,14 +28,17 @@ public class PasswordResetService { private final PasswordResetTokenRepository passwordResetTokenRepository; private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; + private final EmailService emailService; private final SecureRandom secureRandom = new SecureRandom(); public PasswordResetService(PasswordResetTokenRepository passwordResetTokenRepository, UserRepository userRepository, - PasswordEncoder passwordEncoder) { + PasswordEncoder passwordEncoder, + EmailService emailService) { this.passwordResetTokenRepository = passwordResetTokenRepository; this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; + this.emailService = emailService; } @Transactional @@ -66,9 +69,11 @@ public class PasswordResetService { resetToken.setExpiresAt(now.plusMinutes(RESET_TOKEN_MINUTES)); passwordResetTokenRepository.save(resetToken); + emailService.sendPasswordResetLink(managedUser, rawToken); + return new ForgotPasswordResponse( - "If an account matches that username or email, a reset token has been generated.", - rawToken + "If an account matches that username or email, a reset link has been sent.", + null ); } 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 c1239cc3..b53a4c4c 100644 --- a/backend/src/main/java/com/petshop/backend/service/SaleService.java +++ b/backend/src/main/java/com/petshop/backend/service/SaleService.java @@ -31,8 +31,9 @@ public class SaleService { private final UserRepository userRepository; private final CouponRepository couponRepository; private final CartRepository cartRepository; + private final EmailService emailService; - public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CartRepository cartRepository) { + public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CartRepository cartRepository, EmailService emailService) { this.saleRepository = saleRepository; this.productRepository = productRepository; this.storeRepository = storeRepository; @@ -40,6 +41,7 @@ public class SaleService { this.userRepository = userRepository; this.couponRepository = couponRepository; this.cartRepository = cartRepository; + this.emailService = emailService; } @Transactional(readOnly = true) @@ -268,6 +270,11 @@ public class SaleService { sale.setItems(saleItems); Sale savedSale = saleRepository.save(sale); + + if (!Boolean.TRUE.equals(savedSale.getIsRefund()) && savedSale.getCustomer() != null) { + emailService.sendPurchaseReceipt(savedSale); + } + return mapToResponse(savedSale); } diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index e3c3e0df..f16140f7 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -50,6 +50,11 @@ springdoc: app: upload: base-dir: ${UPLOAD_BASE_DIR:uploads} + frontend-url: ${FRONTEND_URL:http://localhost:3000} + +resend: + api-key: ${RESEND_API_KEY:} + from: ${RESEND_FROM:PetShop