feat(back-end): email service and test
This commit is contained in:
@@ -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'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -195,7 +199,11 @@ public class OrderService {
|
|||||||
// Generate Invoice and QR Bill
|
// Generate Invoice and QR Bill
|
||||||
generateAndSaveDocuments(order, savedItems);
|
generateAndSaveDocuments(order, savedItems);
|
||||||
|
|
||||||
return orderRepo.save(order);
|
Order savedOrder = orderRepo.save(order);
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(new OrderCreatedEvent(this, savedOrder));
|
||||||
|
|
||||||
|
return savedOrder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void generateAndSaveDocuments(Order order, List<OrderItem> items) {
|
private void generateAndSaveDocuments(Order order, List<OrderItem> items) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>© 2026 3D-Fab. Tutti i diritti riservati.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user