feat(back-end): email service and test
All checks were successful
Build, Test and Deploy / test-backend (push) Successful in 49s
Build, Test and Deploy / build-and-push (push) Successful in 55s
Build, Test and Deploy / deploy (push) Successful in 8s

This commit is contained in:
2026-02-23 15:23:11 +01:00
parent c3f9539988
commit 0438ba3ae5
10 changed files with 323 additions and 3 deletions

View File

@@ -37,6 +37,7 @@ dependencies {
implementation 'io.github.openhtmltopdf:openhtmltopdf-svg-support:1.1.37' implementation 'io.github.openhtmltopdf:openhtmltopdf-svg-support:1.1.37'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'net.codecrete.qrbill:qrbill-generator:3.4.0' implementation 'net.codecrete.qrbill:qrbill-generator:3.4.0'
implementation 'org.springframework.boot:spring-boot-starter-mail'

View File

@@ -3,12 +3,14 @@ package com.printcalculator;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication @SpringBootApplication
@EnableTransactionManagement @EnableTransactionManagement
@EnableScheduling @EnableScheduling
@EnableAsync
public class BackendApplication { public class BackendApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@@ -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<String> testTemplate() {
Context context = new Context();
Map<String, Object> 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);
}
}

View File

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

View File

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

View File

@@ -8,9 +8,10 @@ import com.printcalculator.repository.OrderItemRepository;
import com.printcalculator.repository.OrderRepository; import com.printcalculator.repository.OrderRepository;
import com.printcalculator.repository.QuoteLineItemRepository; import com.printcalculator.repository.QuoteLineItemRepository;
import com.printcalculator.repository.QuoteSessionRepository; import com.printcalculator.repository.QuoteSessionRepository;
import com.printcalculator.event.OrderCreatedEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
@@ -34,6 +35,7 @@ public class OrderService {
private final StorageService storageService; private final StorageService storageService;
private final InvoicePdfRenderingService invoiceService; private final InvoicePdfRenderingService invoiceService;
private final QrBillService qrBillService; private final QrBillService qrBillService;
private final ApplicationEventPublisher eventPublisher;
public OrderService(OrderRepository orderRepo, public OrderService(OrderRepository orderRepo,
OrderItemRepository orderItemRepo, OrderItemRepository orderItemRepo,
@@ -42,7 +44,8 @@ public class OrderService {
CustomerRepository customerRepo, CustomerRepository customerRepo,
StorageService storageService, StorageService storageService,
InvoicePdfRenderingService invoiceService, InvoicePdfRenderingService invoiceService,
QrBillService qrBillService) { QrBillService qrBillService,
ApplicationEventPublisher eventPublisher) {
this.orderRepo = orderRepo; this.orderRepo = orderRepo;
this.orderItemRepo = orderItemRepo; this.orderItemRepo = orderItemRepo;
this.quoteSessionRepo = quoteSessionRepo; this.quoteSessionRepo = quoteSessionRepo;
@@ -51,6 +54,7 @@ public class OrderService {
this.storageService = storageService; this.storageService = storageService;
this.invoiceService = invoiceService; this.invoiceService = invoiceService;
this.qrBillService = qrBillService; this.qrBillService = qrBillService;
this.eventPublisher = eventPublisher;
} }
@Transactional @Transactional
@@ -194,8 +198,12 @@ public class OrderService {
// Generate Invoice and QR Bill // Generate Invoice and QR Bill
generateAndSaveDocuments(order, savedItems); 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<OrderItem> items) { private void generateAndSaveDocuments(Order order, List<OrderItem> items) {

View File

@@ -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<String, Object> contextData);
}

View File

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

View File

@@ -26,3 +26,16 @@ clamav.enabled=${CLAMAV_ENABLED:false}
# TWINT Configuration # TWINT Configuration
payment.twint.url=${TWINT_PAYMENT_URL:https://go.twint.ch/1/e/tw?tw=acq.gERQQytOTnyIMuQHUqn4hlxgciHE5X7nnqHnNSPAr2OF2K3uBlXJDr2n9JU3sgxa.} 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}

View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Conferma Ordine</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
border-bottom: 1px solid #eeeeee;
padding-bottom: 20px;
margin-bottom: 20px;
}
.header h1 {
color: #333333;
}
.content {
color: #555555;
line-height: 1.6;
}
.order-details {
background-color: #f9f9f9;
padding: 15px;
border-radius: 5px;
margin-top: 20px;
}
.order-details th {
text-align: left;
padding-right: 20px;
color: #333333;
}
.footer {
text-align: center;
font-size: 0.9em;
color: #999999;
margin-top: 30px;
border-top: 1px solid #eeeeee;
padding-top: 20px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Grazie per il tuo ordine!</h1>
</div>
<div class="content">
<p>Ciao <span th:text="${customerName}">Cliente</span>,</p>
<p>Abbiamo ricevuto il tuo ordine e stiamo iniziando a elaborarlo. Ecco un riepilogo dei dettagli:</p>
<div class="order-details">
<table>
<tr>
<th>Numero Ordine:</th>
<td th:text="${orderId}">#0000</td>
</tr>
<tr>
<th>Data:</th>
<td th:text="${orderDate}">01/01/2026</td>
</tr>
<tr>
<th>Costo totale:</th>
<td th:text="${totalCost} + ' CHF'">0.00 CHF</td>
</tr>
</table>
</div>
<p>Se hai domande o dubbi, non esitare a contattarci.</p>
</div>
<div class="footer">
<p>&copy; 2026 3D-Fab. Tutti i diritti riservati.</p>
</div>
</div>
</body>
</html>