diff --git a/backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java b/backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java index 7ae62a7..ab2be20 100644 --- a/backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java +++ b/backend/src/main/java/com/printcalculator/controller/DevEmailTestController.java @@ -33,9 +33,22 @@ public class DevEmailTestController { templateData.put("customerName", "Mario Rossi"); templateData.put("orderId", orderId); 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("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); String html = templateEngine.process("email/order-confirmation", context); diff --git a/backend/src/main/java/com/printcalculator/controller/OrderController.java b/backend/src/main/java/com/printcalculator/controller/OrderController.java index 2bee5b1..7995e75 100644 --- a/backend/src/main/java/com/printcalculator/controller/OrderController.java +++ b/backend/src/main/java/com/printcalculator/controller/OrderController.java @@ -157,6 +157,19 @@ public class OrderController { Order order = orderRepo.findById(orderId) .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 items = orderItemRepo.findByOrder_Id(orderId); Payment payment = paymentRepo.findByOrder_Id(orderId).orElse(null); @@ -169,6 +182,10 @@ public class OrderController { .body(pdf); } + private String buildConfirmationPdfRelativePath(Order order) { + return "orders/" + order.getId() + "/documents/confirmation-" + getDisplayOrderNumber(order) + ".pdf"; + } + @GetMapping("/{orderId}/twint") public ResponseEntity> getTwintPayment(@PathVariable UUID orderId) { Order order = orderRepo.findById(orderId).orElse(null); @@ -239,6 +256,7 @@ public class OrderController { dto.setCustomerEmail(order.getCustomerEmail()); dto.setCustomerPhone(order.getCustomerPhone()); + dto.setPreferredLanguage(order.getPreferredLanguage()); dto.setBillingCustomerType(order.getBillingCustomerType()); dto.setCurrency(order.getCurrency()); dto.setSetupCostChf(order.getSetupCostChf()); diff --git a/backend/src/main/java/com/printcalculator/dto/CreateOrderRequest.java b/backend/src/main/java/com/printcalculator/dto/CreateOrderRequest.java index 2921b2e..5264502 100644 --- a/backend/src/main/java/com/printcalculator/dto/CreateOrderRequest.java +++ b/backend/src/main/java/com/printcalculator/dto/CreateOrderRequest.java @@ -8,6 +8,7 @@ public class CreateOrderRequest { private CustomerDto customer; private AddressDto billingAddress; private AddressDto shippingAddress; + private String language; private boolean shippingSameAsBilling; @AssertTrue(message = "L'accettazione dei Termini e Condizioni e obbligatoria.") diff --git a/backend/src/main/java/com/printcalculator/dto/OrderDto.java b/backend/src/main/java/com/printcalculator/dto/OrderDto.java index eccd46c..63dd666 100644 --- a/backend/src/main/java/com/printcalculator/dto/OrderDto.java +++ b/backend/src/main/java/com/printcalculator/dto/OrderDto.java @@ -13,6 +13,7 @@ public class OrderDto { private String paymentMethod; private String customerEmail; private String customerPhone; + private String preferredLanguage; private String billingCustomerType; private AddressDto billingAddress; private AddressDto shippingAddress; @@ -48,6 +49,9 @@ public class OrderDto { public String getCustomerPhone() { return 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 void setBillingCustomerType(String billingCustomerType) { this.billingCustomerType = billingCustomerType; } diff --git a/backend/src/main/java/com/printcalculator/entity/Order.java b/backend/src/main/java/com/printcalculator/entity/Order.java index 1c59d6c..da1feb1 100644 --- a/backend/src/main/java/com/printcalculator/entity/Order.java +++ b/backend/src/main/java/com/printcalculator/entity/Order.java @@ -95,6 +95,10 @@ public class Order { @Column(name = "shipping_country_code", length = 2) private String shippingCountryCode; + @ColumnDefault("'it'") + @Column(name = "preferred_language", length = 2) + private String preferredLanguage; + @ColumnDefault("'CHF'") @Column(name = "currency", nullable = false, length = 3) private String currency; @@ -356,6 +360,14 @@ public class Order { this.currency = currency; } + public String getPreferredLanguage() { + return preferredLanguage; + } + + public void setPreferredLanguage(String preferredLanguage) { + this.preferredLanguage = preferredLanguage; + } + public BigDecimal getSetupCostChf() { return setupCostChf; } diff --git a/backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java b/backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java index e46fc14..076b128 100644 --- a/backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java +++ b/backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java @@ -4,12 +4,13 @@ import com.printcalculator.entity.Order; import com.printcalculator.entity.OrderItem; import com.printcalculator.entity.Payment; import com.printcalculator.event.OrderCreatedEvent; -import com.printcalculator.event.PaymentReportedEvent; 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.QrBillService; -import com.printcalculator.repository.OrderItemRepository; +import com.printcalculator.service.StorageService; +import com.printcalculator.service.email.EmailNotificationService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; 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.stereotype.Component; +import java.text.NumberFormat; +import java.time.Year; import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.Currency; import java.util.HashMap; +import java.util.List; +import java.util.Locale; import java.util.Map; +import java.nio.file.Paths; @Slf4j @Component @RequiredArgsConstructor public class OrderEmailListener { + private static final String DEFAULT_LANGUAGE = "it"; + private final EmailNotificationService emailNotificationService; private final InvoicePdfRenderingService invoicePdfRenderingService; private final OrderItemRepository orderItemRepository; private final QrBillService qrBillService; + private final StorageService storageService; @Value("${app.mail.admin.enabled:true}") private boolean adminMailEnabled; @@ -48,7 +59,7 @@ public class OrderEmailListener { try { sendCustomerConfirmationEmail(order); - + if (adminMailEnabled && adminMailAddress != null && !adminMailAddress.isEmpty()) { sendAdminNotificationEmail(order); } @@ -85,83 +96,291 @@ public class OrderEmailListener { } private void sendCustomerConfirmationEmail(Order order) { - Map templateData = new HashMap<>(); - templateData.put("customerName", order.getCustomer().getFirstName()); - 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())); + String language = resolveLanguage(order.getPreferredLanguage()); + String orderNumber = getDisplayOrderNumber(order); - emailNotificationService.sendEmail( + Map templateData = buildBaseTemplateData(order, language); + String subject = applyOrderConfirmationTexts(templateData, language, orderNumber); + byte[] confirmationPdf = loadOrGenerateConfirmationPdf(order); + + emailNotificationService.sendEmailWithAttachment( order.getCustomer().getEmail(), - "Conferma Ordine #" + getDisplayOrderNumber(order) + " - 3D-Fab", + subject, "order-confirmation", - templateData + templateData, + buildConfirmationAttachmentName(language, orderNumber), + confirmationPdf ); } private void sendPaymentReportedEmail(Order order) { - Map templateData = new HashMap<>(); - templateData.put("customerName", order.getCustomer().getFirstName()); - templateData.put("orderId", order.getId()); - templateData.put("orderNumber", getDisplayOrderNumber(order)); - templateData.put("orderDetailsUrl", buildOrderDetailsUrl(order)); + String language = resolveLanguage(order.getPreferredLanguage()); + String orderNumber = getDisplayOrderNumber(order); + + Map templateData = buildBaseTemplateData(order, language); + String subject = applyPaymentReportedTexts(templateData, language, orderNumber); emailNotificationService.sendEmail( order.getCustomer().getEmail(), - "Stiamo verificando il tuo pagamento (Ordine #" + getDisplayOrderNumber(order) + ")", + subject, "payment-reported", templateData ); } private void sendPaidInvoiceEmail(Order order, Payment payment) { - Map templateData = new HashMap<>(); - templateData.put("customerName", order.getCustomer().getFirstName()); - templateData.put("orderId", order.getId()); - templateData.put("orderNumber", getDisplayOrderNumber(order)); - templateData.put("orderDetailsUrl", buildOrderDetailsUrl(order)); - templateData.put("totalCost", String.format("%.2f", order.getTotalChf())); + String language = resolveLanguage(order.getPreferredLanguage()); + String orderNumber = getDisplayOrderNumber(order); + + Map templateData = buildBaseTemplateData(order, language); + String subject = applyPaymentConfirmedTexts(templateData, language, orderNumber); byte[] pdf = null; try { - java.util.List items = orderItemRepository.findByOrder_Id(order.getId()); + List items = orderItemRepository.findByOrder_Id(order.getId()); pdf = invoicePdfRenderingService.generateDocumentPdf(order, items, false, qrBillService, payment); } catch (Exception e) { log.error("Failed to generate PDF for paid invoice email: {}", e.getMessage(), e); } - String filename = "Fattura-" + getDisplayOrderNumber(order) + ".pdf"; - emailNotificationService.sendEmailWithAttachment( order.getCustomer().getEmail(), - "Fattura Pagata (Ordine #" + getDisplayOrderNumber(order) + ") - 3D-Fab", + subject, "payment-confirmed", templateData, - filename, + buildPaidInvoiceAttachmentName(language, orderNumber), pdf ); } 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("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())); - - // Possiamo riutilizzare lo stesso template per ora o crearne uno ad-hoc in futuro + String orderNumber = getDisplayOrderNumber(order); + Map templateData = buildBaseTemplateData(order, DEFAULT_LANGUAGE); + templateData.put("customerName", buildCustomerFullName(order)); + + templateData.put("emailTitle", "Nuovo ordine ricevuto"); + templateData.put("headlineText", "Nuovo ordine #" + orderNumber); + templateData.put("greetingText", "Ciao team,"); + templateData.put("introText", "Un nuovo ordine e' stato creato dal cliente."); + 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( adminMailAddress, - "Nuovo Ordine Ricevuto #" + getDisplayOrderNumber(order) + " - " + order.getCustomer().getLastName(), - "order-confirmation", + "Nuovo Ordine Ricevuto #" + orderNumber + " - " + buildCustomerFullName(order), + "order-confirmation", templateData ); } + private Map buildBaseTemplateData(Order order, String language) { + Locale locale = localeForLanguage(language); + NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance(locale); + currencyFormatter.setCurrency(Currency.getInstance("CHF")); + + Map 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 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 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 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) { String orderNumber = order.getOrderNumber(); if (orderNumber != null && !orderNumber.isBlank()) { @@ -170,8 +389,108 @@ public class OrderEmailListener { 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("/+$", ""); - 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 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; + }; } } diff --git a/backend/src/main/java/com/printcalculator/service/OrderService.java b/backend/src/main/java/com/printcalculator/service/OrderService.java index e757a86..2b951da 100644 --- a/backend/src/main/java/com/printcalculator/service/OrderService.java +++ b/backend/src/main/java/com/printcalculator/service/OrderService.java @@ -1,9 +1,7 @@ package com.printcalculator.service; -import com.printcalculator.dto.AddressDto; import com.printcalculator.dto.CreateOrderRequest; import com.printcalculator.entity.*; -import com.printcalculator.entity.Payment; import com.printcalculator.repository.CustomerRepository; import com.printcalculator.repository.OrderItemRepository; import com.printcalculator.repository.OrderRepository; @@ -18,14 +16,11 @@ import org.springframework.transaction.annotation.Transactional; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.OffsetDateTime; -import java.time.format.DateTimeFormatter; import java.util.*; -import java.util.stream.Collectors; @Service public class OrderService { @@ -113,6 +108,7 @@ public class OrderService { order.setStatus("PENDING_PAYMENT"); order.setCreatedAt(OffsetDateTime.now()); order.setUpdatedAt(OffsetDateTime.now()); + order.setPreferredLanguage(normalizeLanguage(request.getLanguage())); order.setCurrency("CHF"); order.setBillingCustomerType(request.getCustomer().getCustomerType()); @@ -281,75 +277,13 @@ public class OrderService { private void generateAndSaveDocuments(Order order, List items) { try { - // 1. Generate QR Bill + // 1. Generate and save the raw QR Bill for internal traceability. 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 - if (qrBillSvg.contains(" 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> invoiceLineItems = items.stream().map(i -> { - Map 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 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 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); + // 2. Generate and save the same confirmation PDF served by /api/orders/{id}/confirmation. + byte[] confirmationPdfBytes = invoiceService.generateDocumentPdf(order, items, true, qrBillService, null); + saveFileBytes(confirmationPdfBytes, buildConfirmationPdfRelativePath(order)); } catch (Exception e) { e.printStackTrace(); @@ -387,4 +321,28 @@ public class OrderService { } 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"; + }; + } } diff --git a/backend/src/main/resources/templates/email/order-confirmation.html b/backend/src/main/resources/templates/email/order-confirmation.html index 708accf..37a6082 100644 --- a/backend/src/main/resources/templates/email/order-confirmation.html +++ b/backend/src/main/resources/templates/email/order-confirmation.html @@ -2,7 +2,7 @@ - Conferma Ordine + Order Confirmation -
-
-

Grazie per il tuo ordine #00000000

-
-
-

Ciao Cliente,

-

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

- -
- - - - - - - - - - - - - -
Numero Ordine:00000000
Data:01/01/2026
Costo totale:0.00 CHF
-
- -

- Clicca qui per i dettagli: - https://tuosito.it/ordine/00000000-0000-0000-0000-000000000000 -

- -

Se hai domande o dubbi, non esitare a contattarci.

-
- +
+
+

Thank you for your order #00000000

+ +
+

Hi Customer,

+

We received your order and started processing it.

+ +
+

Order details

+ + + + + + + + + + + + + +
Order number00000000
DateJan 1, 2026, 10:00:00 AM
TotalCHF 0.00
+
+ +

+ View order status: + https://example.com/en/co/00000000-0000-0000-0000-000000000000 +

+ +

The order confirmation PDF is attached.

+

If you have questions, reply to this email.

+
+ + +
diff --git a/db.sql b/db.sql index a55b16a..327547a 100644 --- a/db.sql +++ b/db.sql @@ -468,6 +468,7 @@ CREATE TABLE IF NOT EXISTS orders customer_id uuid REFERENCES customers (customer_id), customer_email text NOT NULL, customer_phone text, + preferred_language char(2) NOT NULL DEFAULT 'it', -- Snapshot indirizzo/fatturazione (evita tabella addresses e mantiene storico) billing_customer_type text NOT NULL CHECK (billing_customer_type IN ('PRIVATE', 'COMPANY')), diff --git a/deploy/envs/dev.env b/deploy/envs/dev.env index 88f337d..eb400c1 100644 --- a/deploy/envs/dev.env +++ b/deploy/envs/dev.env @@ -10,4 +10,4 @@ FRONTEND_PORT=18082 CLAMAV_HOST=192.168.1.147 CLAMAV_PORT=3310 CLAMAV_ENABLED=true - +APP_FRONTEND_BASE_URL=http://localhost:18082 diff --git a/deploy/envs/int.env b/deploy/envs/int.env index 1353b58..d989460 100644 --- a/deploy/envs/int.env +++ b/deploy/envs/int.env @@ -10,4 +10,4 @@ FRONTEND_PORT=18081 CLAMAV_HOST=192.168.1.147 CLAMAV_PORT=3310 CLAMAV_ENABLED=true - +APP_FRONTEND_BASE_URL=http://localhost:18081 diff --git a/deploy/envs/prod.env b/deploy/envs/prod.env index a91bbcb..ce5da60 100644 --- a/deploy/envs/prod.env +++ b/deploy/envs/prod.env @@ -10,4 +10,4 @@ FRONTEND_PORT=80 CLAMAV_HOST=192.168.1.147 CLAMAV_PORT=3310 CLAMAV_ENABLED=true - +APP_FRONTEND_BASE_URL=https://3d-fab.ch diff --git a/docker-compose.deploy.yml b/docker-compose.deploy.yml index 912b2c1..9f14474 100644 --- a/docker-compose.deploy.yml +++ b/docker-compose.deploy.yml @@ -22,6 +22,7 @@ services: - APP_MAIL_FROM=${APP_MAIL_FROM:-info@3d-fab.ch} - APP_MAIL_ADMIN_ENABLED=${APP_MAIL_ADMIN_ENABLED:-true} - 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 - PROFILES_DIR=/app/profiles restart: always diff --git a/frontend/src/app/features/checkout/checkout.component.ts b/frontend/src/app/features/checkout/checkout.component.ts index 06f6288..bd343e4 100644 --- a/frontend/src/app/features/checkout/checkout.component.ts +++ b/frontend/src/app/features/checkout/checkout.component.ts @@ -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 { AppCardComponent } from '../../shared/components/app-card/app-card.component'; import { AppToggleSelectorComponent, ToggleOption } from '../../shared/components/app-toggle-selector/app-toggle-selector.component'; +import { LanguageService } from '../../core/services/language.service'; @Component({ selector: 'app-checkout', @@ -29,6 +30,7 @@ export class CheckoutComponent implements OnInit { private quoteService = inject(QuoteEstimatorService); private router = inject(Router); private route = inject(ActivatedRoute); + private languageService = inject(LanguageService); checkoutForm: FormGroup; sessionId: string | null = null; @@ -191,6 +193,7 @@ export class CheckoutComponent implements OnInit { countryCode: formVal.shippingAddress.countryCode }, shippingSameAsBilling: formVal.shippingSameAsBilling, + language: this.languageService.selectedLang(), acceptTerms: formVal.acceptLegal, acceptPrivacy: formVal.acceptLegal }; diff --git a/frontend/src/assets/i18n/en.json b/frontend/src/assets/i18n/en.json index 9d7f4e1..c9fb65c 100644 --- a/frontend/src/assets/i18n/en.json +++ b/frontend/src/assets/i18n/en.json @@ -132,8 +132,8 @@ "ABOUT": { "TITLE": "About Us", "EYEBROW": "3D Printing Lab", - "SUBTITLE": "Transparency on price, quality and time. Technical and CAD consultation for businesses and individuals.", - "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.", + "SUBTITLE": "We are two students with a strong desire to build and learn.", + "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", "PASSION_BIKE_TRIAL": "Bike trial", "PASSION_MOUNTAIN": "Mountain",