From 0438ba3ae59f1fe0b5e41d2888e567d1b284d35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 23 Feb 2026 15:23:11 +0100 Subject: [PATCH] feat(back-end): email service and test --- backend/build.gradle | 1 + .../printcalculator/BackendApplication.java | 2 + .../controller/DevEmailTestController.java | 44 ++++++++++ .../event/OrderCreatedEvent.java | 16 ++++ .../event/listener/OrderEmailListener.java | 77 ++++++++++++++++ .../printcalculator/service/OrderService.java | 14 ++- .../email/EmailNotificationService.java | 17 ++++ .../email/SmtpEmailNotificationService.java | 54 ++++++++++++ .../src/main/resources/application.properties | 13 +++ .../templates/email/order-confirmation.html | 88 +++++++++++++++++++ 10 files changed, 323 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java create mode 100644 backend/src/main/java/com/printcalculator/event/OrderCreatedEvent.java create mode 100644 backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java create mode 100644 backend/src/main/java/com/printcalculator/service/email/EmailNotificationService.java create mode 100644 backend/src/main/java/com/printcalculator/service/email/SmtpEmailNotificationService.java create mode 100644 backend/src/main/resources/templates/email/order-confirmation.html diff --git a/backend/build.gradle b/backend/build.gradle index b15d8ef..b465785 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -37,6 +37,7 @@ dependencies { implementation 'io.github.openhtmltopdf:openhtmltopdf-svg-support:1.1.37' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'net.codecrete.qrbill:qrbill-generator:3.4.0' + implementation 'org.springframework.boot:spring-boot-starter-mail' diff --git a/backend/src/main/java/com/printcalculator/BackendApplication.java b/backend/src/main/java/com/printcalculator/BackendApplication.java index e9ee050..c528427 100644 --- a/backend/src/main/java/com/printcalculator/BackendApplication.java +++ b/backend/src/main/java/com/printcalculator/BackendApplication.java @@ -3,12 +3,14 @@ package com.printcalculator; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication @EnableTransactionManagement @EnableScheduling +@EnableAsync public class BackendApplication { public static void main(String[] args) { diff --git a/backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java b/backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java new file mode 100644 index 0000000..e60726d --- /dev/null +++ b/backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java @@ -0,0 +1,44 @@ +package com.printcalculator.controller; + +import org.springframework.context.annotation.Profile; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@RestController +@RequestMapping("/api/dev/email") +@Profile("local") +public class DevEmailTestController { + + private final TemplateEngine templateEngine; + + public DevEmailTestController(TemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + @GetMapping("/test-template") + public ResponseEntity testTemplate() { + Context context = new Context(); + Map templateData = new HashMap<>(); + templateData.put("customerName", "Mario Rossi"); + templateData.put("orderId", UUID.randomUUID()); + templateData.put("orderDate", OffsetDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"))); + templateData.put("totalCost", "45.50"); + + context.setVariables(templateData); + String html = templateEngine.process("email/order-confirmation", context); + + return ResponseEntity.ok() + .header("Content-Type", "text/html; charset=utf-8") + .body(html); + } +} diff --git a/backend/src/main/java/com/printcalculator/event/OrderCreatedEvent.java b/backend/src/main/java/com/printcalculator/event/OrderCreatedEvent.java new file mode 100644 index 0000000..29ccde1 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/event/OrderCreatedEvent.java @@ -0,0 +1,16 @@ +package com.printcalculator.event; + +import com.printcalculator.entity.Order; +import lombok.Getter; +import org.springframework.context.ApplicationEvent; + +@Getter +public class OrderCreatedEvent extends ApplicationEvent { + + private final Order order; + + public OrderCreatedEvent(Object source, Order order) { + super(source); + this.order = order; + } +} diff --git a/backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java b/backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java new file mode 100644 index 0000000..18b95cc --- /dev/null +++ b/backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java @@ -0,0 +1,77 @@ +package com.printcalculator.event.listener; + +import com.printcalculator.entity.Order; +import com.printcalculator.event.OrderCreatedEvent; +import com.printcalculator.service.email.EmailNotificationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OrderEmailListener { + + private final EmailNotificationService emailNotificationService; + + @Value("${app.mail.admin.enabled:true}") + private boolean adminMailEnabled; + + @Value("${app.mail.admin.address:}") + private String adminMailAddress; + + @Async + @EventListener + public void handleOrderCreatedEvent(OrderCreatedEvent event) { + Order order = event.getOrder(); + log.info("Processing OrderCreatedEvent for order id: {}", order.getId()); + + try { + sendCustomerConfirmationEmail(order); + + if (adminMailEnabled && adminMailAddress != null && !adminMailAddress.isEmpty()) { + sendAdminNotificationEmail(order); + } + } catch (Exception e) { + log.error("Failed to process email notifications for order id: {}", order.getId(), e); + } + } + + private void sendCustomerConfirmationEmail(Order order) { + Map templateData = new HashMap<>(); + templateData.put("customerName", order.getCustomer().getFirstName()); + templateData.put("orderId", order.getId()); + templateData.put("orderDate", order.getCreatedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"))); + templateData.put("totalCost", String.format("%.2f", order.getTotalChf())); + + emailNotificationService.sendEmail( + order.getCustomer().getEmail(), + "Conferma Ordine #" + order.getId() + " - 3D-Fab", + "order-confirmation", + templateData + ); + } + + private void sendAdminNotificationEmail(Order order) { + Map templateData = new HashMap<>(); + templateData.put("customerName", order.getCustomer().getFirstName() + " " + order.getCustomer().getLastName()); + templateData.put("orderId", order.getId()); + templateData.put("orderDate", order.getCreatedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"))); + templateData.put("totalCost", String.format("%.2f", order.getTotalChf())); + + // Possiamo riutilizzare lo stesso template per ora o crearne uno ad-hoc in futuro + emailNotificationService.sendEmail( + adminMailAddress, + "Nuovo Ordine Ricevuto #" + order.getId() + " - " + order.getCustomer().getLastName(), + "order-confirmation", + templateData + ); + } +} diff --git a/backend/src/main/java/com/printcalculator/service/OrderService.java b/backend/src/main/java/com/printcalculator/service/OrderService.java index 02d3289..569269a 100644 --- a/backend/src/main/java/com/printcalculator/service/OrderService.java +++ b/backend/src/main/java/com/printcalculator/service/OrderService.java @@ -8,9 +8,10 @@ import com.printcalculator.repository.OrderItemRepository; import com.printcalculator.repository.OrderRepository; import com.printcalculator.repository.QuoteLineItemRepository; import com.printcalculator.repository.QuoteSessionRepository; +import com.printcalculator.event.OrderCreatedEvent; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.math.BigDecimal; @@ -34,6 +35,7 @@ public class OrderService { private final StorageService storageService; private final InvoicePdfRenderingService invoiceService; private final QrBillService qrBillService; + private final ApplicationEventPublisher eventPublisher; public OrderService(OrderRepository orderRepo, OrderItemRepository orderItemRepo, @@ -42,7 +44,8 @@ public class OrderService { CustomerRepository customerRepo, StorageService storageService, InvoicePdfRenderingService invoiceService, - QrBillService qrBillService) { + QrBillService qrBillService, + ApplicationEventPublisher eventPublisher) { this.orderRepo = orderRepo; this.orderItemRepo = orderItemRepo; this.quoteSessionRepo = quoteSessionRepo; @@ -51,6 +54,7 @@ public class OrderService { this.storageService = storageService; this.invoiceService = invoiceService; this.qrBillService = qrBillService; + this.eventPublisher = eventPublisher; } @Transactional @@ -194,8 +198,12 @@ public class OrderService { // Generate Invoice and QR Bill generateAndSaveDocuments(order, savedItems); + + Order savedOrder = orderRepo.save(order); + + eventPublisher.publishEvent(new OrderCreatedEvent(this, savedOrder)); - return orderRepo.save(order); + return savedOrder; } private void generateAndSaveDocuments(Order order, List items) { diff --git a/backend/src/main/java/com/printcalculator/service/email/EmailNotificationService.java b/backend/src/main/java/com/printcalculator/service/email/EmailNotificationService.java new file mode 100644 index 0000000..5c2448a --- /dev/null +++ b/backend/src/main/java/com/printcalculator/service/email/EmailNotificationService.java @@ -0,0 +1,17 @@ +package com.printcalculator.service.email; + +import java.util.Map; + +public interface EmailNotificationService { + + /** + * Sends an HTML email using a Thymeleaf template. + * + * @param to The recipient email address. + * @param subject The subject of the email. + * @param templateName The name of the Thymeleaf template (e.g., "order-confirmation"). + * @param contextData The data to populate the template with. + */ + void sendEmail(String to, String subject, String templateName, Map contextData); + +} diff --git a/backend/src/main/java/com/printcalculator/service/email/SmtpEmailNotificationService.java b/backend/src/main/java/com/printcalculator/service/email/SmtpEmailNotificationService.java new file mode 100644 index 0000000..3e0987b --- /dev/null +++ b/backend/src/main/java/com/printcalculator/service/email/SmtpEmailNotificationService.java @@ -0,0 +1,54 @@ +package com.printcalculator.service.email; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; +import org.thymeleaf.TemplateEngine; +import org.thymeleaf.context.Context; + +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class SmtpEmailNotificationService implements EmailNotificationService { + + private final JavaMailSender emailSender; + private final TemplateEngine templateEngine; + + @Value("${app.mail.from}") + private String fromAddress; + + @Override + public void sendEmail(String to, String subject, String templateName, Map contextData) { + log.info("Preparing to send email to {} with template {}", to, templateName); + + try { + Context context = new Context(); + context.setVariables(contextData); + + String process = templateEngine.process("email/" + templateName, context); + MimeMessage mimeMessage = emailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8"); + + helper.setFrom(fromAddress); + helper.setTo(to); + helper.setSubject(subject); + helper.setText(process, true); // true indicates HTML format + + emailSender.send(mimeMessage); + log.info("Email successfully sent to {}", to); + + } catch (MessagingException e) { + log.error("Failed to send email to {}", to, e); + // Non blocco l'ordine se l'email fallisce, ma loggo l'errore adeguatamente. + } catch (Exception e) { + log.error("Unexpected error while sending email to {}", to, e); + } + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 82887cc..7297d1e 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -26,3 +26,16 @@ clamav.enabled=${CLAMAV_ENABLED:false} # TWINT Configuration payment.twint.url=${TWINT_PAYMENT_URL:https://go.twint.ch/1/e/tw?tw=acq.gERQQytOTnyIMuQHUqn4hlxgciHE5X7nnqHnNSPAr2OF2K3uBlXJDr2n9JU3sgxa.} + +# Mail Configuration +spring.mail.host=${MAIL_HOST:mail.infomaniak.com} +spring.mail.port=${MAIL_PORT:587} +spring.mail.username=${MAIL_USERNAME:info@3d-fab.ch} +spring.mail.password=${MAIL_PASSWORD:ht*44k+Tq39R+R-O} +spring.mail.properties.mail.smtp.auth=${MAIL_SMTP_AUTH:false} +spring.mail.properties.mail.smtp.starttls.enable=${MAIL_SMTP_STARTTLS:false} + +# Application Mail Settings +app.mail.from=${APP_MAIL_FROM:noreply@printcalculator.local} +app.mail.admin.enabled=${APP_MAIL_ADMIN_ENABLED:true} +app.mail.admin.address=${APP_MAIL_ADMIN_ADDRESS:admin@printcalculator.local} diff --git a/backend/src/main/resources/templates/email/order-confirmation.html b/backend/src/main/resources/templates/email/order-confirmation.html new file mode 100644 index 0000000..fb75eea --- /dev/null +++ b/backend/src/main/resources/templates/email/order-confirmation.html @@ -0,0 +1,88 @@ + + + + + Conferma Ordine + + + +
+
+

Grazie per il tuo ordine!

+
+
+

Ciao Cliente,

+

Abbiamo ricevuto il tuo ordine e stiamo iniziando a elaborarlo. Ecco un riepilogo dei dettagli:

+ +
+ + + + + + + + + + + + + +
Numero Ordine:#0000
Data:01/01/2026
Costo totale:0.00 CHF
+
+ +

Se hai domande o dubbi, non esitare a contattarci.

+
+ +
+ +