feat(back-end): email improvements
Some checks failed
Build, Test and Deploy / build-and-push (push) Has been cancelled
Build, Test and Deploy / deploy (push) Has been cancelled
Build, Test and Deploy / test-backend (push) Has been cancelled

This commit is contained in:
2026-02-27 12:09:37 +01:00
parent 6e52988cdd
commit 8e9afbf260
15 changed files with 506 additions and 159 deletions

View File

@@ -33,9 +33,22 @@ public class DevEmailTestController {
templateData.put("customerName", "Mario Rossi"); templateData.put("customerName", "Mario Rossi");
templateData.put("orderId", orderId); templateData.put("orderId", orderId);
templateData.put("orderNumber", orderId.toString().split("-")[0]); templateData.put("orderNumber", orderId.toString().split("-")[0]);
templateData.put("orderDetailsUrl", "https://tuosito.it/ordine/" + orderId); templateData.put("orderDetailsUrl", "https://tuosito.it/it/co/" + orderId);
templateData.put("orderDate", OffsetDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"))); templateData.put("orderDate", OffsetDateTime.now().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")));
templateData.put("totalCost", "45.50"); templateData.put("totalCost", "CHF 45.50");
templateData.put("currentYear", OffsetDateTime.now().getYear());
templateData.put("emailTitle", "Conferma ordine");
templateData.put("headlineText", "Grazie per il tuo ordine #" + templateData.get("orderNumber"));
templateData.put("greetingText", "Ciao Mario,");
templateData.put("introText", "Abbiamo ricevuto il tuo ordine e iniziato l'elaborazione.");
templateData.put("detailsTitleText", "Dettagli ordine");
templateData.put("labelOrderNumber", "Numero ordine");
templateData.put("labelDate", "Data");
templateData.put("labelTotal", "Totale");
templateData.put("orderDetailsCtaText", "Visualizza stato ordine");
templateData.put("attachmentHintText", "In allegato trovi la conferma ordine in PDF con QR bill.");
templateData.put("supportText", "Se hai domande, rispondi a questa email.");
templateData.put("footerText", "Messaggio automatico di 3D-Fab.");
context.setVariables(templateData); context.setVariables(templateData);
String html = templateEngine.process("email/order-confirmation", context); String html = templateEngine.process("email/order-confirmation", context);

View File

@@ -157,6 +157,19 @@ public class OrderController {
Order order = orderRepo.findById(orderId) Order order = orderRepo.findById(orderId)
.orElseThrow(() -> new RuntimeException("Order not found")); .orElseThrow(() -> new RuntimeException("Order not found"));
if (isConfirmation) {
String relativePath = buildConfirmationPdfRelativePath(order);
try {
byte[] existingPdf = storageService.loadAsResource(Paths.get(relativePath)).getInputStream().readAllBytes();
return ResponseEntity.ok()
.header("Content-Disposition", "attachment; filename=\"confirmation-" + getDisplayOrderNumber(order) + ".pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(existingPdf);
} catch (Exception ignored) {
// Fallback to on-the-fly generation if the stored file is missing or unreadable.
}
}
List<OrderItem> items = orderItemRepo.findByOrder_Id(orderId); List<OrderItem> items = orderItemRepo.findByOrder_Id(orderId);
Payment payment = paymentRepo.findByOrder_Id(orderId).orElse(null); Payment payment = paymentRepo.findByOrder_Id(orderId).orElse(null);
@@ -169,6 +182,10 @@ public class OrderController {
.body(pdf); .body(pdf);
} }
private String buildConfirmationPdfRelativePath(Order order) {
return "orders/" + order.getId() + "/documents/confirmation-" + getDisplayOrderNumber(order) + ".pdf";
}
@GetMapping("/{orderId}/twint") @GetMapping("/{orderId}/twint")
public ResponseEntity<Map<String, String>> getTwintPayment(@PathVariable UUID orderId) { public ResponseEntity<Map<String, String>> getTwintPayment(@PathVariable UUID orderId) {
Order order = orderRepo.findById(orderId).orElse(null); Order order = orderRepo.findById(orderId).orElse(null);
@@ -239,6 +256,7 @@ public class OrderController {
dto.setCustomerEmail(order.getCustomerEmail()); dto.setCustomerEmail(order.getCustomerEmail());
dto.setCustomerPhone(order.getCustomerPhone()); dto.setCustomerPhone(order.getCustomerPhone());
dto.setPreferredLanguage(order.getPreferredLanguage());
dto.setBillingCustomerType(order.getBillingCustomerType()); dto.setBillingCustomerType(order.getBillingCustomerType());
dto.setCurrency(order.getCurrency()); dto.setCurrency(order.getCurrency());
dto.setSetupCostChf(order.getSetupCostChf()); dto.setSetupCostChf(order.getSetupCostChf());

View File

@@ -8,6 +8,7 @@ public class CreateOrderRequest {
private CustomerDto customer; private CustomerDto customer;
private AddressDto billingAddress; private AddressDto billingAddress;
private AddressDto shippingAddress; private AddressDto shippingAddress;
private String language;
private boolean shippingSameAsBilling; private boolean shippingSameAsBilling;
@AssertTrue(message = "L'accettazione dei Termini e Condizioni e obbligatoria.") @AssertTrue(message = "L'accettazione dei Termini e Condizioni e obbligatoria.")

View File

@@ -13,6 +13,7 @@ public class OrderDto {
private String paymentMethod; private String paymentMethod;
private String customerEmail; private String customerEmail;
private String customerPhone; private String customerPhone;
private String preferredLanguage;
private String billingCustomerType; private String billingCustomerType;
private AddressDto billingAddress; private AddressDto billingAddress;
private AddressDto shippingAddress; private AddressDto shippingAddress;
@@ -48,6 +49,9 @@ public class OrderDto {
public String getCustomerPhone() { return customerPhone; } public String getCustomerPhone() { return customerPhone; }
public void setCustomerPhone(String customerPhone) { this.customerPhone = customerPhone; } public void setCustomerPhone(String customerPhone) { this.customerPhone = customerPhone; }
public String getPreferredLanguage() { return preferredLanguage; }
public void setPreferredLanguage(String preferredLanguage) { this.preferredLanguage = preferredLanguage; }
public String getBillingCustomerType() { return billingCustomerType; } public String getBillingCustomerType() { return billingCustomerType; }
public void setBillingCustomerType(String billingCustomerType) { this.billingCustomerType = billingCustomerType; } public void setBillingCustomerType(String billingCustomerType) { this.billingCustomerType = billingCustomerType; }

View File

@@ -95,6 +95,10 @@ public class Order {
@Column(name = "shipping_country_code", length = 2) @Column(name = "shipping_country_code", length = 2)
private String shippingCountryCode; private String shippingCountryCode;
@ColumnDefault("'it'")
@Column(name = "preferred_language", length = 2)
private String preferredLanguage;
@ColumnDefault("'CHF'") @ColumnDefault("'CHF'")
@Column(name = "currency", nullable = false, length = 3) @Column(name = "currency", nullable = false, length = 3)
private String currency; private String currency;
@@ -356,6 +360,14 @@ public class Order {
this.currency = currency; this.currency = currency;
} }
public String getPreferredLanguage() {
return preferredLanguage;
}
public void setPreferredLanguage(String preferredLanguage) {
this.preferredLanguage = preferredLanguage;
}
public BigDecimal getSetupCostChf() { public BigDecimal getSetupCostChf() {
return setupCostChf; return setupCostChf;
} }

View File

@@ -4,12 +4,13 @@ import com.printcalculator.entity.Order;
import com.printcalculator.entity.OrderItem; import com.printcalculator.entity.OrderItem;
import com.printcalculator.entity.Payment; import com.printcalculator.entity.Payment;
import com.printcalculator.event.OrderCreatedEvent; import com.printcalculator.event.OrderCreatedEvent;
import com.printcalculator.event.PaymentReportedEvent;
import com.printcalculator.event.PaymentConfirmedEvent; import com.printcalculator.event.PaymentConfirmedEvent;
import com.printcalculator.service.email.EmailNotificationService; import com.printcalculator.event.PaymentReportedEvent;
import com.printcalculator.repository.OrderItemRepository;
import com.printcalculator.service.InvoicePdfRenderingService; import com.printcalculator.service.InvoicePdfRenderingService;
import com.printcalculator.service.QrBillService; import com.printcalculator.service.QrBillService;
import com.printcalculator.repository.OrderItemRepository; import com.printcalculator.service.StorageService;
import com.printcalculator.service.email.EmailNotificationService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -17,19 +18,29 @@ import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.text.NumberFormat;
import java.time.Year;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
import java.util.Currency;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.nio.file.Paths;
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class OrderEmailListener { public class OrderEmailListener {
private static final String DEFAULT_LANGUAGE = "it";
private final EmailNotificationService emailNotificationService; private final EmailNotificationService emailNotificationService;
private final InvoicePdfRenderingService invoicePdfRenderingService; private final InvoicePdfRenderingService invoicePdfRenderingService;
private final OrderItemRepository orderItemRepository; private final OrderItemRepository orderItemRepository;
private final QrBillService qrBillService; private final QrBillService qrBillService;
private final StorageService storageService;
@Value("${app.mail.admin.enabled:true}") @Value("${app.mail.admin.enabled:true}")
private boolean adminMailEnabled; private boolean adminMailEnabled;
@@ -48,7 +59,7 @@ public class OrderEmailListener {
try { try {
sendCustomerConfirmationEmail(order); sendCustomerConfirmationEmail(order);
if (adminMailEnabled && adminMailAddress != null && !adminMailAddress.isEmpty()) { if (adminMailEnabled && adminMailAddress != null && !adminMailAddress.isEmpty()) {
sendAdminNotificationEmail(order); sendAdminNotificationEmail(order);
} }
@@ -85,83 +96,291 @@ public class OrderEmailListener {
} }
private void sendCustomerConfirmationEmail(Order order) { private void sendCustomerConfirmationEmail(Order order) {
Map<String, Object> templateData = new HashMap<>(); String language = resolveLanguage(order.getPreferredLanguage());
templateData.put("customerName", order.getCustomer().getFirstName()); String orderNumber = getDisplayOrderNumber(order);
templateData.put("orderId", order.getId());
templateData.put("orderNumber", getDisplayOrderNumber(order));
templateData.put("orderDetailsUrl", buildOrderDetailsUrl(order));
templateData.put("orderDate", order.getCreatedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")));
templateData.put("totalCost", String.format("%.2f", order.getTotalChf()));
emailNotificationService.sendEmail( Map<String, Object> templateData = buildBaseTemplateData(order, language);
String subject = applyOrderConfirmationTexts(templateData, language, orderNumber);
byte[] confirmationPdf = loadOrGenerateConfirmationPdf(order);
emailNotificationService.sendEmailWithAttachment(
order.getCustomer().getEmail(), order.getCustomer().getEmail(),
"Conferma Ordine #" + getDisplayOrderNumber(order) + " - 3D-Fab", subject,
"order-confirmation", "order-confirmation",
templateData templateData,
buildConfirmationAttachmentName(language, orderNumber),
confirmationPdf
); );
} }
private void sendPaymentReportedEmail(Order order) { private void sendPaymentReportedEmail(Order order) {
Map<String, Object> templateData = new HashMap<>(); String language = resolveLanguage(order.getPreferredLanguage());
templateData.put("customerName", order.getCustomer().getFirstName()); String orderNumber = getDisplayOrderNumber(order);
templateData.put("orderId", order.getId());
templateData.put("orderNumber", getDisplayOrderNumber(order)); Map<String, Object> templateData = buildBaseTemplateData(order, language);
templateData.put("orderDetailsUrl", buildOrderDetailsUrl(order)); String subject = applyPaymentReportedTexts(templateData, language, orderNumber);
emailNotificationService.sendEmail( emailNotificationService.sendEmail(
order.getCustomer().getEmail(), order.getCustomer().getEmail(),
"Stiamo verificando il tuo pagamento (Ordine #" + getDisplayOrderNumber(order) + ")", subject,
"payment-reported", "payment-reported",
templateData templateData
); );
} }
private void sendPaidInvoiceEmail(Order order, Payment payment) { private void sendPaidInvoiceEmail(Order order, Payment payment) {
Map<String, Object> templateData = new HashMap<>(); String language = resolveLanguage(order.getPreferredLanguage());
templateData.put("customerName", order.getCustomer().getFirstName()); String orderNumber = getDisplayOrderNumber(order);
templateData.put("orderId", order.getId());
templateData.put("orderNumber", getDisplayOrderNumber(order)); Map<String, Object> templateData = buildBaseTemplateData(order, language);
templateData.put("orderDetailsUrl", buildOrderDetailsUrl(order)); String subject = applyPaymentConfirmedTexts(templateData, language, orderNumber);
templateData.put("totalCost", String.format("%.2f", order.getTotalChf()));
byte[] pdf = null; byte[] pdf = null;
try { try {
java.util.List<OrderItem> items = orderItemRepository.findByOrder_Id(order.getId()); List<OrderItem> items = orderItemRepository.findByOrder_Id(order.getId());
pdf = invoicePdfRenderingService.generateDocumentPdf(order, items, false, qrBillService, payment); pdf = invoicePdfRenderingService.generateDocumentPdf(order, items, false, qrBillService, payment);
} catch (Exception e) { } catch (Exception e) {
log.error("Failed to generate PDF for paid invoice email: {}", e.getMessage(), e); log.error("Failed to generate PDF for paid invoice email: {}", e.getMessage(), e);
} }
String filename = "Fattura-" + getDisplayOrderNumber(order) + ".pdf";
emailNotificationService.sendEmailWithAttachment( emailNotificationService.sendEmailWithAttachment(
order.getCustomer().getEmail(), order.getCustomer().getEmail(),
"Fattura Pagata (Ordine #" + getDisplayOrderNumber(order) + ") - 3D-Fab", subject,
"payment-confirmed", "payment-confirmed",
templateData, templateData,
filename, buildPaidInvoiceAttachmentName(language, orderNumber),
pdf pdf
); );
} }
private void sendAdminNotificationEmail(Order order) { private void sendAdminNotificationEmail(Order order) {
Map<String, Object> templateData = new HashMap<>(); String orderNumber = getDisplayOrderNumber(order);
templateData.put("customerName", order.getCustomer().getFirstName() + " " + order.getCustomer().getLastName()); Map<String, Object> templateData = buildBaseTemplateData(order, DEFAULT_LANGUAGE);
templateData.put("orderId", order.getId()); templateData.put("customerName", buildCustomerFullName(order));
templateData.put("orderNumber", getDisplayOrderNumber(order));
templateData.put("orderDetailsUrl", buildOrderDetailsUrl(order)); templateData.put("emailTitle", "Nuovo ordine ricevuto");
templateData.put("orderDate", order.getCreatedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm"))); templateData.put("headlineText", "Nuovo ordine #" + orderNumber);
templateData.put("totalCost", String.format("%.2f", order.getTotalChf())); templateData.put("greetingText", "Ciao team,");
templateData.put("introText", "Un nuovo ordine e' stato creato dal cliente.");
// Possiamo riutilizzare lo stesso template per ora o crearne uno ad-hoc in futuro templateData.put("detailsTitleText", "Dettagli ordine");
templateData.put("labelOrderNumber", "Numero ordine");
templateData.put("labelDate", "Data");
templateData.put("labelTotal", "Totale");
templateData.put("orderDetailsCtaText", "Apri dettaglio ordine");
templateData.put("attachmentHintText", "La conferma cliente e il QR bill sono stati salvati nella cartella documenti dell'ordine.");
templateData.put("supportText", "Controlla i dettagli e procedi con la gestione operativa.");
templateData.put("footerText", "Notifica automatica sistema ordini.");
emailNotificationService.sendEmail( emailNotificationService.sendEmail(
adminMailAddress, adminMailAddress,
"Nuovo Ordine Ricevuto #" + getDisplayOrderNumber(order) + " - " + order.getCustomer().getLastName(), "Nuovo Ordine Ricevuto #" + orderNumber + " - " + buildCustomerFullName(order),
"order-confirmation", "order-confirmation",
templateData templateData
); );
} }
private Map<String, Object> buildBaseTemplateData(Order order, String language) {
Locale locale = localeForLanguage(language);
NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale);
currencyFormatter.setCurrency(Currency.getInstance("CHF"));
Map<String, Object> templateData = new HashMap<>();
templateData.put("customerName", buildCustomerFirstName(order, language));
templateData.put("orderId", order.getId());
templateData.put("orderNumber", getDisplayOrderNumber(order));
templateData.put("orderDetailsUrl", buildOrderDetailsUrl(order, language));
templateData.put(
"orderDate",
order.getCreatedAt().format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).withLocale(locale))
);
templateData.put("totalCost", currencyFormatter.format(order.getTotalChf()));
templateData.put("currentYear", Year.now().getValue());
return templateData;
}
private String applyOrderConfirmationTexts(Map<String, Object> templateData, String language, String orderNumber) {
return switch (language) {
case "en" -> {
templateData.put("emailTitle", "Order Confirmation");
templateData.put("headlineText", "Thank you for your order #" + orderNumber);
templateData.put("greetingText", "Hi " + templateData.get("customerName") + ",");
templateData.put("introText", "We received your order and started processing it.");
templateData.put("detailsTitleText", "Order details");
templateData.put("labelOrderNumber", "Order number");
templateData.put("labelDate", "Date");
templateData.put("labelTotal", "Total");
templateData.put("orderDetailsCtaText", "View order status");
templateData.put("attachmentHintText", "Attached you can find the order confirmation PDF with the QR bill.");
templateData.put("supportText", "If you have questions, reply to this email and we will help you.");
templateData.put("footerText", "Automated message from 3D-Fab.");
yield "Order Confirmation #" + orderNumber + " - 3D-Fab";
}
case "de" -> {
templateData.put("emailTitle", "Bestellbestaetigung");
templateData.put("headlineText", "Danke fuer Ihre Bestellung #" + orderNumber);
templateData.put("greetingText", "Hallo " + templateData.get("customerName") + ",");
templateData.put("introText", "Wir haben Ihre Bestellung erhalten und mit der Bearbeitung begonnen.");
templateData.put("detailsTitleText", "Bestelldetails");
templateData.put("labelOrderNumber", "Bestellnummer");
templateData.put("labelDate", "Datum");
templateData.put("labelTotal", "Gesamtbetrag");
templateData.put("orderDetailsCtaText", "Bestellstatus ansehen");
templateData.put("attachmentHintText", "Im Anhang finden Sie die Bestellbestaetigung mit QR-Rechnung.");
templateData.put("supportText", "Bei Fragen antworten Sie einfach auf diese E-Mail.");
templateData.put("footerText", "Automatische Nachricht von 3D-Fab.");
yield "Bestellbestaetigung #" + orderNumber + " - 3D-Fab";
}
case "fr" -> {
templateData.put("emailTitle", "Confirmation de commande");
templateData.put("headlineText", "Merci pour votre commande #" + orderNumber);
templateData.put("greetingText", "Bonjour " + templateData.get("customerName") + ",");
templateData.put("introText", "Nous avons recu votre commande et commence son traitement.");
templateData.put("detailsTitleText", "Details de commande");
templateData.put("labelOrderNumber", "Numero de commande");
templateData.put("labelDate", "Date");
templateData.put("labelTotal", "Total");
templateData.put("orderDetailsCtaText", "Voir le statut de la commande");
templateData.put("attachmentHintText", "Vous trouverez en piece jointe la confirmation de commande avec la facture QR.");
templateData.put("supportText", "Si vous avez des questions, repondez a cet email.");
templateData.put("footerText", "Message automatique de 3D-Fab.");
yield "Confirmation de commande #" + orderNumber + " - 3D-Fab";
}
default -> {
templateData.put("emailTitle", "Conferma ordine");
templateData.put("headlineText", "Grazie per il tuo ordine #" + orderNumber);
templateData.put("greetingText", "Ciao " + templateData.get("customerName") + ",");
templateData.put("introText", "Abbiamo ricevuto il tuo ordine e iniziato l'elaborazione.");
templateData.put("detailsTitleText", "Dettagli ordine");
templateData.put("labelOrderNumber", "Numero ordine");
templateData.put("labelDate", "Data");
templateData.put("labelTotal", "Totale");
templateData.put("orderDetailsCtaText", "Visualizza stato ordine");
templateData.put("attachmentHintText", "In allegato trovi la conferma ordine in PDF con QR bill.");
templateData.put("supportText", "Se hai domande, rispondi a questa email e ti aiutiamo subito.");
templateData.put("footerText", "Messaggio automatico di 3D-Fab.");
yield "Conferma Ordine #" + orderNumber + " - 3D-Fab";
}
};
}
private String applyPaymentReportedTexts(Map<String, Object> templateData, String language, String orderNumber) {
return switch (language) {
case "en" -> {
templateData.put("emailTitle", "Payment Reported");
templateData.put("headlineText", "Payment reported for order #" + orderNumber);
templateData.put("greetingText", "Hi " + templateData.get("customerName") + ",");
templateData.put("introText", "We received your payment report and our team is now verifying it.");
templateData.put("statusText", "Current status: Payment under verification.");
templateData.put("orderDetailsCtaText", "Check order status");
templateData.put("supportText", "You will receive another email as soon as the payment is confirmed.");
templateData.put("footerText", "Automated message from 3D-Fab.");
templateData.put("labelOrderNumber", "Order number");
templateData.put("labelTotal", "Total");
yield "We are verifying your payment (Order #" + orderNumber + ")";
}
case "de" -> {
templateData.put("emailTitle", "Zahlung gemeldet");
templateData.put("headlineText", "Zahlung fuer Bestellung #" + orderNumber + " gemeldet");
templateData.put("greetingText", "Hallo " + templateData.get("customerName") + ",");
templateData.put("introText", "Wir haben Ihre Zahlungsmitteilung erhalten und pruefen sie aktuell.");
templateData.put("statusText", "Aktueller Status: Zahlung in Pruefung.");
templateData.put("orderDetailsCtaText", "Bestellstatus ansehen");
templateData.put("supportText", "Sobald die Zahlung bestaetigt ist, erhalten Sie eine weitere E-Mail.");
templateData.put("footerText", "Automatische Nachricht von 3D-Fab.");
templateData.put("labelOrderNumber", "Bestellnummer");
templateData.put("labelTotal", "Gesamtbetrag");
yield "Wir pruefen Ihre Zahlung (Bestellung #" + orderNumber + ")";
}
case "fr" -> {
templateData.put("emailTitle", "Paiement signale");
templateData.put("headlineText", "Paiement signale pour la commande #" + orderNumber);
templateData.put("greetingText", "Bonjour " + templateData.get("customerName") + ",");
templateData.put("introText", "Nous avons recu votre signalement de paiement et nous le verifions.");
templateData.put("statusText", "Statut actuel: Paiement en verification.");
templateData.put("orderDetailsCtaText", "Consulter le statut de la commande");
templateData.put("supportText", "Vous recevrez un nouvel email des que le paiement sera confirme.");
templateData.put("footerText", "Message automatique de 3D-Fab.");
templateData.put("labelOrderNumber", "Numero de commande");
templateData.put("labelTotal", "Total");
yield "Nous verifions votre paiement (Commande #" + orderNumber + ")";
}
default -> {
templateData.put("emailTitle", "Pagamento segnalato");
templateData.put("headlineText", "Pagamento segnalato per ordine #" + orderNumber);
templateData.put("greetingText", "Ciao " + templateData.get("customerName") + ",");
templateData.put("introText", "Abbiamo ricevuto la tua segnalazione di pagamento e la stiamo verificando.");
templateData.put("statusText", "Stato attuale: pagamento in verifica.");
templateData.put("orderDetailsCtaText", "Controlla lo stato ordine");
templateData.put("supportText", "Riceverai una nuova email non appena il pagamento sara' confermato.");
templateData.put("footerText", "Messaggio automatico di 3D-Fab.");
templateData.put("labelOrderNumber", "Numero ordine");
templateData.put("labelTotal", "Totale");
yield "Stiamo verificando il tuo pagamento (Ordine #" + orderNumber + ")";
}
};
}
private String applyPaymentConfirmedTexts(Map<String, Object> templateData, String language, String orderNumber) {
return switch (language) {
case "en" -> {
templateData.put("emailTitle", "Payment Confirmed");
templateData.put("headlineText", "Payment confirmed for order #" + orderNumber);
templateData.put("greetingText", "Hi " + templateData.get("customerName") + ",");
templateData.put("introText", "Your payment has been confirmed and the order moved into production.");
templateData.put("statusText", "Current status: In production.");
templateData.put("attachmentHintText", "The paid invoice PDF is attached to this email.");
templateData.put("orderDetailsCtaText", "View order status");
templateData.put("supportText", "We will notify you again when the shipment is ready.");
templateData.put("footerText", "Automated message from 3D-Fab.");
templateData.put("labelOrderNumber", "Order number");
templateData.put("labelTotal", "Total");
yield "Payment confirmed (Order #" + orderNumber + ") - 3D-Fab";
}
case "de" -> {
templateData.put("emailTitle", "Zahlung bestaetigt");
templateData.put("headlineText", "Zahlung fuer Bestellung #" + orderNumber + " bestaetigt");
templateData.put("greetingText", "Hallo " + templateData.get("customerName") + ",");
templateData.put("introText", "Ihre Zahlung wurde bestaetigt und die Bestellung ist jetzt in Produktion.");
templateData.put("statusText", "Aktueller Status: In Produktion.");
templateData.put("attachmentHintText", "Die bezahlte Rechnung als PDF ist dieser E-Mail beigefuegt.");
templateData.put("orderDetailsCtaText", "Bestellstatus ansehen");
templateData.put("supportText", "Wir informieren Sie erneut, sobald der Versand bereit ist.");
templateData.put("footerText", "Automatische Nachricht von 3D-Fab.");
templateData.put("labelOrderNumber", "Bestellnummer");
templateData.put("labelTotal", "Gesamtbetrag");
yield "Zahlung bestaetigt (Bestellung #" + orderNumber + ") - 3D-Fab";
}
case "fr" -> {
templateData.put("emailTitle", "Paiement confirme");
templateData.put("headlineText", "Paiement confirme pour la commande #" + orderNumber);
templateData.put("greetingText", "Bonjour " + templateData.get("customerName") + ",");
templateData.put("introText", "Votre paiement est confirme et la commande est passe en production.");
templateData.put("statusText", "Statut actuel: En production.");
templateData.put("attachmentHintText", "La facture payee en PDF est jointe a cet email.");
templateData.put("orderDetailsCtaText", "Voir le statut de la commande");
templateData.put("supportText", "Nous vous informerons a nouveau des que l'expedition sera prete.");
templateData.put("footerText", "Message automatique de 3D-Fab.");
templateData.put("labelOrderNumber", "Numero de commande");
templateData.put("labelTotal", "Total");
yield "Paiement confirme (Commande #" + orderNumber + ") - 3D-Fab";
}
default -> {
templateData.put("emailTitle", "Pagamento confermato");
templateData.put("headlineText", "Pagamento confermato per ordine #" + orderNumber);
templateData.put("greetingText", "Ciao " + templateData.get("customerName") + ",");
templateData.put("introText", "Il tuo pagamento e' stato confermato e l'ordine e' entrato in produzione.");
templateData.put("statusText", "Stato attuale: in produzione.");
templateData.put("attachmentHintText", "In allegato trovi la fattura saldata in PDF.");
templateData.put("orderDetailsCtaText", "Visualizza stato ordine");
templateData.put("supportText", "Ti aggiorneremo di nuovo quando la spedizione sara' pronta.");
templateData.put("footerText", "Messaggio automatico di 3D-Fab.");
templateData.put("labelOrderNumber", "Numero ordine");
templateData.put("labelTotal", "Totale");
yield "Pagamento confermato (Ordine #" + orderNumber + ") - 3D-Fab";
}
};
}
private String getDisplayOrderNumber(Order order) { private String getDisplayOrderNumber(Order order) {
String orderNumber = order.getOrderNumber(); String orderNumber = order.getOrderNumber();
if (orderNumber != null && !orderNumber.isBlank()) { if (orderNumber != null && !orderNumber.isBlank()) {
@@ -170,8 +389,108 @@ public class OrderEmailListener {
return order.getId() != null ? order.getId().toString() : "unknown"; return order.getId() != null ? order.getId().toString() : "unknown";
} }
private String buildOrderDetailsUrl(Order order) { private String buildOrderDetailsUrl(Order order, String language) {
String baseUrl = frontendBaseUrl == null ? "" : frontendBaseUrl.replaceAll("/+$", ""); String baseUrl = frontendBaseUrl == null ? "" : frontendBaseUrl.replaceAll("/+$", "");
return baseUrl + "/co/" + order.getId(); return baseUrl + "/" + language + "/co/" + order.getId();
}
private String buildConfirmationAttachmentName(String language, String orderNumber) {
return switch (language) {
case "en" -> "Order-Confirmation-" + orderNumber + ".pdf";
case "de" -> "Bestellbestaetigung-" + orderNumber + ".pdf";
case "fr" -> "Confirmation-Commande-" + orderNumber + ".pdf";
default -> "Conferma-Ordine-" + orderNumber + ".pdf";
};
}
private String buildPaidInvoiceAttachmentName(String language, String orderNumber) {
return switch (language) {
case "en" -> "Paid-Invoice-" + orderNumber + ".pdf";
case "de" -> "Bezahlte-Rechnung-" + orderNumber + ".pdf";
case "fr" -> "Facture-Payee-" + orderNumber + ".pdf";
default -> "Fattura-Pagata-" + orderNumber + ".pdf";
};
}
private byte[] loadOrGenerateConfirmationPdf(Order order) {
byte[] stored = loadStoredConfirmationPdf(order);
if (stored != null) {
return stored;
}
try {
List<OrderItem> items = orderItemRepository.findByOrder_Id(order.getId());
return invoicePdfRenderingService.generateDocumentPdf(order, items, true, qrBillService, null);
} catch (Exception e) {
log.error("Failed to generate fallback confirmation PDF for order id: {}", order.getId(), e);
return null;
}
}
private byte[] loadStoredConfirmationPdf(Order order) {
String relativePath = buildConfirmationPdfRelativePath(order);
try {
return storageService.loadAsResource(Paths.get(relativePath)).getInputStream().readAllBytes();
} catch (Exception e) {
log.warn("Confirmation PDF not found for order id {} at {}", order.getId(), relativePath);
return null;
}
}
private String buildConfirmationPdfRelativePath(Order order) {
return "orders/" + order.getId() + "/documents/confirmation-" + getDisplayOrderNumber(order) + ".pdf";
}
private String buildCustomerFirstName(Order order, String language) {
if (order.getCustomer() != null && order.getCustomer().getFirstName() != null && !order.getCustomer().getFirstName().isBlank()) {
return order.getCustomer().getFirstName();
}
if (order.getBillingFirstName() != null && !order.getBillingFirstName().isBlank()) {
return order.getBillingFirstName();
}
return switch (language) {
case "en" -> "Customer";
case "de" -> "Kunde";
case "fr" -> "Client";
default -> "Cliente";
};
}
private String buildCustomerFullName(Order order) {
String firstName = order.getCustomer() != null ? order.getCustomer().getFirstName() : null;
String lastName = order.getCustomer() != null ? order.getCustomer().getLastName() : null;
if (firstName != null && !firstName.isBlank() && lastName != null && !lastName.isBlank()) {
return firstName + " " + lastName;
}
if (order.getBillingFirstName() != null && !order.getBillingFirstName().isBlank()
&& order.getBillingLastName() != null && !order.getBillingLastName().isBlank()) {
return order.getBillingFirstName() + " " + order.getBillingLastName();
}
return "Cliente";
}
private Locale localeForLanguage(String language) {
return switch (language) {
case "en" -> Locale.ENGLISH;
case "de" -> Locale.GERMAN;
case "fr" -> Locale.FRENCH;
default -> Locale.ITALIAN;
};
}
private String resolveLanguage(String language) {
if (language == null || language.isBlank()) {
return DEFAULT_LANGUAGE;
}
String normalized = language.trim().toLowerCase(Locale.ROOT);
if (normalized.length() > 2) {
normalized = normalized.substring(0, 2);
}
return switch (normalized) {
case "it", "en", "de", "fr" -> normalized;
default -> DEFAULT_LANGUAGE;
};
} }
} }

View File

@@ -1,9 +1,7 @@
package com.printcalculator.service; package com.printcalculator.service;
import com.printcalculator.dto.AddressDto;
import com.printcalculator.dto.CreateOrderRequest; import com.printcalculator.dto.CreateOrderRequest;
import com.printcalculator.entity.*; import com.printcalculator.entity.*;
import com.printcalculator.entity.Payment;
import com.printcalculator.repository.CustomerRepository; import com.printcalculator.repository.CustomerRepository;
import com.printcalculator.repository.OrderItemRepository; import com.printcalculator.repository.OrderItemRepository;
import com.printcalculator.repository.OrderRepository; import com.printcalculator.repository.OrderRepository;
@@ -18,14 +16,11 @@ import org.springframework.transaction.annotation.Transactional;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
@Service @Service
public class OrderService { public class OrderService {
@@ -113,6 +108,7 @@ public class OrderService {
order.setStatus("PENDING_PAYMENT"); order.setStatus("PENDING_PAYMENT");
order.setCreatedAt(OffsetDateTime.now()); order.setCreatedAt(OffsetDateTime.now());
order.setUpdatedAt(OffsetDateTime.now()); order.setUpdatedAt(OffsetDateTime.now());
order.setPreferredLanguage(normalizeLanguage(request.getLanguage()));
order.setCurrency("CHF"); order.setCurrency("CHF");
order.setBillingCustomerType(request.getCustomer().getCustomerType()); order.setBillingCustomerType(request.getCustomer().getCustomerType());
@@ -281,75 +277,13 @@ public class OrderService {
private void generateAndSaveDocuments(Order order, List<OrderItem> items) { private void generateAndSaveDocuments(Order order, List<OrderItem> items) {
try { try {
// 1. Generate QR Bill // 1. Generate and save the raw QR Bill for internal traceability.
byte[] qrBillSvgBytes = qrBillService.generateQrBillSvg(order); byte[] qrBillSvgBytes = qrBillService.generateQrBillSvg(order);
String qrBillSvg = new String(qrBillSvgBytes, StandardCharsets.UTF_8); saveFileBytes(qrBillSvgBytes, buildQrBillSvgRelativePath(order));
// Strip XML declaration and DOCTYPE if present, as they validity break the embedding HTML page // 2. Generate and save the same confirmation PDF served by /api/orders/{id}/confirmation.
if (qrBillSvg.contains("<?xml")) { byte[] confirmationPdfBytes = invoiceService.generateDocumentPdf(order, items, true, qrBillService, null);
int svgStartIndex = qrBillSvg.indexOf("<svg"); saveFileBytes(confirmationPdfBytes, buildConfirmationPdfRelativePath(order));
if (svgStartIndex != -1) {
qrBillSvg = qrBillSvg.substring(svgStartIndex);
}
}
// Save QR Bill SVG
String qrRelativePath = "orders/" + order.getId() + "/documents/qr-bill.svg";
saveFileBytes(qrBillSvgBytes, qrRelativePath);
// 2. Prepare Invoice Variables
Map<String, Object> vars = new HashMap<>();
vars.put("sellerDisplayName", "3D Fab Switzerland");
vars.put("sellerAddressLine1", "Sede Ticino, Svizzera");
vars.put("sellerAddressLine2", "Sede Bienne, Svizzera");
vars.put("sellerEmail", "info@3dfab.ch");
vars.put("invoiceNumber", "INV-" + getDisplayOrderNumber(order).toUpperCase());
vars.put("invoiceDate", order.getCreatedAt().format(DateTimeFormatter.ISO_LOCAL_DATE));
vars.put("dueDate", order.getCreatedAt().plusDays(7).format(DateTimeFormatter.ISO_LOCAL_DATE));
String buyerName = "BUSINESS".equals(order.getBillingCustomerType())
? order.getBillingCompanyName()
: order.getBillingFirstName() + " " + order.getBillingLastName();
vars.put("buyerDisplayName", buyerName);
vars.put("buyerAddressLine1", order.getBillingAddressLine1());
vars.put("buyerAddressLine2", order.getBillingZip() + " " + order.getBillingCity() + ", " + order.getBillingCountryCode());
List<Map<String, Object>> invoiceLineItems = items.stream().map(i -> {
Map<String, Object> line = new HashMap<>();
line.put("description", "Stampa 3D: " + i.getOriginalFilename());
line.put("quantity", i.getQuantity());
line.put("unitPriceFormatted", String.format("CHF %.2f", i.getUnitPriceChf()));
line.put("lineTotalFormatted", String.format("CHF %.2f", i.getLineTotalChf()));
return line;
}).collect(Collectors.toList());
Map<String, Object> setupLine = new HashMap<>();
setupLine.put("description", "Costo Setup");
setupLine.put("quantity", 1);
setupLine.put("unitPriceFormatted", String.format("CHF %.2f", order.getSetupCostChf()));
setupLine.put("lineTotalFormatted", String.format("CHF %.2f", order.getSetupCostChf()));
invoiceLineItems.add(setupLine);
Map<String, Object> shippingLine = new HashMap<>();
shippingLine.put("description", "Spedizione");
shippingLine.put("quantity", 1);
shippingLine.put("unitPriceFormatted", String.format("CHF %.2f", order.getShippingCostChf()));
shippingLine.put("lineTotalFormatted", String.format("CHF %.2f", order.getShippingCostChf()));
invoiceLineItems.add(shippingLine);
vars.put("invoiceLineItems", invoiceLineItems);
vars.put("subtotalFormatted", String.format("CHF %.2f", order.getSubtotalChf()));
vars.put("grandTotalFormatted", String.format("CHF %.2f", order.getTotalChf()));
vars.put("paymentTermsText", "Appena riceviamo il pagamento l'ordine entrerà nella coda di stampa. Grazie per la fiducia");
// 3. Generate PDF
Payment payment = null; // New order, payment not received yet
byte[] pdfBytes = invoiceService.generateInvoicePdfBytesFromTemplate(vars, qrBillSvg);
// Save PDF
String pdfRelativePath = "orders/" + order.getId() + "/documents/invoice-" + order.getId() + ".pdf";
saveFileBytes(pdfBytes, pdfRelativePath);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@@ -387,4 +321,28 @@ public class OrderService {
} }
return order.getId() != null ? order.getId().toString() : "unknown"; return order.getId() != null ? order.getId().toString() : "unknown";
} }
private String buildQrBillSvgRelativePath(Order order) {
return "orders/" + order.getId() + "/documents/qr-bill.svg";
}
private String buildConfirmationPdfRelativePath(Order order) {
return "orders/" + order.getId() + "/documents/confirmation-" + getDisplayOrderNumber(order) + ".pdf";
}
private String normalizeLanguage(String language) {
if (language == null || language.isBlank()) {
return "it";
}
String normalized = language.trim().toLowerCase(Locale.ROOT);
if (normalized.length() > 2) {
normalized = normalized.substring(0, 2);
}
return switch (normalized) {
case "it", "en", "de", "fr" -> normalized;
default -> "it";
};
}
} }

View File

@@ -2,7 +2,7 @@
<html xmlns:th="http://www.thymeleaf.org"> <html xmlns:th="http://www.thymeleaf.org">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Conferma Ordine</title> <title th:text="${emailTitle}">Order Confirmation</title>
<style> <style>
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
@@ -10,6 +10,7 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
.container { .container {
max-width: 600px; max-width: 600px;
margin: 20px auto; margin: 20px auto;
@@ -18,30 +19,41 @@
border-radius: 8px; border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
} }
.header { .header {
text-align: center; text-align: center;
border-bottom: 1px solid #eeeeee; border-bottom: 1px solid #eeeeee;
padding-bottom: 20px; padding-bottom: 20px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.header h1 { .header h1 {
color: #333333; color: #333333;
} }
.content { .content {
color: #555555; color: #555555;
line-height: 1.6; line-height: 1.6;
} }
.order-details { .order-details {
background-color: #f9f9f9; background-color: #f9f9f9;
padding: 15px; padding: 15px;
border-radius: 5px; border-radius: 5px;
margin-top: 20px; margin-top: 20px;
} }
.order-details th { .order-details th {
text-align: left; text-align: left;
padding-right: 20px; padding-right: 20px;
color: #333333; color: #333333;
vertical-align: top;
} }
.order-details td {
word-break: break-word;
}
.footer { .footer {
text-align: center; text-align: center;
font-size: 0.9em; font-size: 0.9em;
@@ -53,41 +65,46 @@
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="header"> <div class="header">
<h1>Grazie per il tuo ordine #<span th:text="${orderNumber}">00000000</span></h1> <h1 th:text="${headlineText}">Thank you for your order #00000000</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="${orderNumber}">00000000</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>
Clicca qui per i dettagli:
<a th:href="${orderDetailsUrl}" th:text="${orderDetailsUrl}">https://tuosito.it/ordine/00000000-0000-0000-0000-000000000000</a>
</p>
<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> </div>
<div class="content">
<p th:text="${greetingText}">Hi Customer,</p>
<p th:text="${introText}">We received your order and started processing it.</p>
<div class="order-details">
<p style="margin-top:0; font-weight: bold;" th:text="${detailsTitleText}">Order details</p>
<table>
<tr>
<th th:text="${labelOrderNumber}">Order number</th>
<td th:text="${orderNumber}">00000000</td>
</tr>
<tr>
<th th:text="${labelDate}">Date</th>
<td th:text="${orderDate}">Jan 1, 2026, 10:00:00 AM</td>
</tr>
<tr>
<th th:text="${labelTotal}">Total</th>
<td th:text="${totalCost}">CHF 0.00</td>
</tr>
</table>
</div>
<p>
<span th:text="${orderDetailsCtaText}">View order status</span>:
<a th:href="${orderDetailsUrl}" th:text="${orderDetailsUrl}">https://example.com/en/co/00000000-0000-0000-0000-000000000000</a>
</p>
<p th:text="${attachmentHintText}">The order confirmation PDF is attached.</p>
<p th:text="${supportText}">If you have questions, reply to this email.</p>
</div>
<div class="footer">
<p>&copy; <span th:text="${currentYear}">2026</span> 3D-Fab</p>
<p th:text="${footerText}">Automated message.</p>
</div>
</div>
</body> </body>
</html> </html>

1
db.sql
View File

@@ -468,6 +468,7 @@ CREATE TABLE IF NOT EXISTS orders
customer_id uuid REFERENCES customers (customer_id), customer_id uuid REFERENCES customers (customer_id),
customer_email text NOT NULL, customer_email text NOT NULL,
customer_phone text, customer_phone text,
preferred_language char(2) NOT NULL DEFAULT 'it',
-- Snapshot indirizzo/fatturazione (evita tabella addresses e mantiene storico) -- Snapshot indirizzo/fatturazione (evita tabella addresses e mantiene storico)
billing_customer_type text NOT NULL CHECK (billing_customer_type IN ('PRIVATE', 'COMPANY')), billing_customer_type text NOT NULL CHECK (billing_customer_type IN ('PRIVATE', 'COMPANY')),

View File

@@ -10,4 +10,4 @@ FRONTEND_PORT=18082
CLAMAV_HOST=192.168.1.147 CLAMAV_HOST=192.168.1.147
CLAMAV_PORT=3310 CLAMAV_PORT=3310
CLAMAV_ENABLED=true CLAMAV_ENABLED=true
APP_FRONTEND_BASE_URL=http://localhost:18082

View File

@@ -10,4 +10,4 @@ FRONTEND_PORT=18081
CLAMAV_HOST=192.168.1.147 CLAMAV_HOST=192.168.1.147
CLAMAV_PORT=3310 CLAMAV_PORT=3310
CLAMAV_ENABLED=true CLAMAV_ENABLED=true
APP_FRONTEND_BASE_URL=http://localhost:18081

View File

@@ -10,4 +10,4 @@ FRONTEND_PORT=80
CLAMAV_HOST=192.168.1.147 CLAMAV_HOST=192.168.1.147
CLAMAV_PORT=3310 CLAMAV_PORT=3310
CLAMAV_ENABLED=true CLAMAV_ENABLED=true
APP_FRONTEND_BASE_URL=https://3d-fab.ch

View File

@@ -22,6 +22,7 @@ services:
- APP_MAIL_FROM=${APP_MAIL_FROM:-info@3d-fab.ch} - APP_MAIL_FROM=${APP_MAIL_FROM:-info@3d-fab.ch}
- APP_MAIL_ADMIN_ENABLED=${APP_MAIL_ADMIN_ENABLED:-true} - APP_MAIL_ADMIN_ENABLED=${APP_MAIL_ADMIN_ENABLED:-true}
- APP_MAIL_ADMIN_ADDRESS=${APP_MAIL_ADMIN_ADDRESS:-info@3d-fab.ch} - APP_MAIL_ADMIN_ADDRESS=${APP_MAIL_ADMIN_ADDRESS:-info@3d-fab.ch}
- APP_FRONTEND_BASE_URL=${APP_FRONTEND_BASE_URL:-http://localhost:4200}
- TEMP_DIR=/app/temp - TEMP_DIR=/app/temp
- PROFILES_DIR=/app/profiles - PROFILES_DIR=/app/profiles
restart: always restart: always

View File

@@ -8,6 +8,7 @@ import { AppInputComponent } from '../../shared/components/app-input/app-input.c
import { AppButtonComponent } from '../../shared/components/app-button/app-button.component'; import { AppButtonComponent } from '../../shared/components/app-button/app-button.component';
import { AppCardComponent } from '../../shared/components/app-card/app-card.component'; import { AppCardComponent } from '../../shared/components/app-card/app-card.component';
import { AppToggleSelectorComponent, ToggleOption } from '../../shared/components/app-toggle-selector/app-toggle-selector.component'; import { AppToggleSelectorComponent, ToggleOption } from '../../shared/components/app-toggle-selector/app-toggle-selector.component';
import { LanguageService } from '../../core/services/language.service';
@Component({ @Component({
selector: 'app-checkout', selector: 'app-checkout',
@@ -29,6 +30,7 @@ export class CheckoutComponent implements OnInit {
private quoteService = inject(QuoteEstimatorService); private quoteService = inject(QuoteEstimatorService);
private router = inject(Router); private router = inject(Router);
private route = inject(ActivatedRoute); private route = inject(ActivatedRoute);
private languageService = inject(LanguageService);
checkoutForm: FormGroup; checkoutForm: FormGroup;
sessionId: string | null = null; sessionId: string | null = null;
@@ -191,6 +193,7 @@ export class CheckoutComponent implements OnInit {
countryCode: formVal.shippingAddress.countryCode countryCode: formVal.shippingAddress.countryCode
}, },
shippingSameAsBilling: formVal.shippingSameAsBilling, shippingSameAsBilling: formVal.shippingSameAsBilling,
language: this.languageService.selectedLang(),
acceptTerms: formVal.acceptLegal, acceptTerms: formVal.acceptLegal,
acceptPrivacy: formVal.acceptLegal acceptPrivacy: formVal.acceptLegal
}; };

View File

@@ -132,8 +132,8 @@
"ABOUT": { "ABOUT": {
"TITLE": "About Us", "TITLE": "About Us",
"EYEBROW": "3D Printing Lab", "EYEBROW": "3D Printing Lab",
"SUBTITLE": "Transparency on price, quality and time. Technical and CAD consultation for businesses and individuals.", "SUBTITLE": "We are two students with a strong desire to build and learn.",
"HOW_TEXT": "We offer an automatic quote for those who already have the 3D file, and a consultation path for those who need to design or optimize the model.", "HOW_TEXT": "3D Fab was born from Matteo's initial interest in 3D printing. He bought a printer and started experimenting seriously. \n At a certain point, the first requests arrived: a broken part to replace, a spare part that cannot be found, a handy adapter to have. The requests increased and we said: okay, let's do it properly.\nLater we created a calculator to understand the cost in advance: it was one of the first steps that took us from \"let's make a few parts\" to a real project, together.",
"PASSIONS_TITLE": "Our passions", "PASSIONS_TITLE": "Our passions",
"PASSION_BIKE_TRIAL": "Bike trial", "PASSION_BIKE_TRIAL": "Bike trial",
"PASSION_MOUNTAIN": "Mountain", "PASSION_MOUNTAIN": "Mountain",