dev #8
@@ -140,72 +140,30 @@ public class OrderController {
|
||||
return getOrder(orderId);
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/confirmation")
|
||||
public ResponseEntity<byte[]> getConfirmation(@PathVariable UUID orderId) {
|
||||
return generateDocument(orderId, true);
|
||||
}
|
||||
|
||||
@GetMapping("/{orderId}/invoice")
|
||||
public ResponseEntity<byte[]> getInvoice(@PathVariable UUID orderId) {
|
||||
// Paid invoices are sent by email after back-office payment confirmation.
|
||||
// The public endpoint must not expose a "paid" invoice download.
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
private ResponseEntity<byte[]> generateDocument(UUID orderId, boolean isConfirmation) {
|
||||
Order order = orderRepo.findById(orderId)
|
||||
.orElseThrow(() -> new RuntimeException("Order not found"));
|
||||
|
||||
List<OrderItem> items = orderItemRepo.findByOrder_Id(orderId);
|
||||
Payment payment = paymentRepo.findByOrder_Id(orderId).orElse(null);
|
||||
|
||||
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 = order.getBillingCustomerType().equals("BUSINESS")
|
||||
? 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", "Pagamento entro 7 giorni via Bonifico o TWINT. Grazie.");
|
||||
|
||||
String qrBillSvg = new String(qrBillService.generateQrBillSvg(order), java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
// Strip XML declaration and DOCTYPE if present, as they validity break the embedding HTML page
|
||||
if (qrBillSvg.contains("<?xml")) {
|
||||
int svgStartIndex = qrBillSvg.indexOf("<svg");
|
||||
if (svgStartIndex != -1) {
|
||||
qrBillSvg = qrBillSvg.substring(svgStartIndex);
|
||||
}
|
||||
}
|
||||
|
||||
byte[] pdf = invoiceService.generateInvoicePdfBytesFromTemplate(vars, qrBillSvg);
|
||||
|
||||
byte[] pdf = invoiceService.generateDocumentPdf(order, items, isConfirmation, qrBillService, payment);
|
||||
String typePrefix = isConfirmation ? "confirmation-" : "invoice-";
|
||||
String truncatedUuid = order.getId().toString().substring(0, 8);
|
||||
return ResponseEntity.ok()
|
||||
.header("Content-Disposition", "attachment; filename=\"invoice-" + getDisplayOrderNumber(order) + ".pdf\"")
|
||||
.header("Content-Disposition", "attachment; filename=\"" + typePrefix + truncatedUuid + ".pdf\"")
|
||||
.contentType(MediaType.APPLICATION_PDF)
|
||||
.body(pdf);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.printcalculator.event;
|
||||
|
||||
import com.printcalculator.entity.Order;
|
||||
import com.printcalculator.entity.Payment;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
public class PaymentConfirmedEvent extends ApplicationEvent {
|
||||
private final Order order;
|
||||
private final Payment payment;
|
||||
|
||||
public PaymentConfirmedEvent(Object source, Order order, Payment payment) {
|
||||
super(source);
|
||||
this.order = order;
|
||||
this.payment = payment;
|
||||
}
|
||||
|
||||
public Order getOrder() {
|
||||
return order;
|
||||
}
|
||||
|
||||
public Payment getPayment() {
|
||||
return payment;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.printcalculator.event.listener;
|
||||
|
||||
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.service.InvoicePdfRenderingService;
|
||||
import com.printcalculator.service.QrBillService;
|
||||
import com.printcalculator.repository.OrderItemRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -22,6 +27,9 @@ import java.util.Map;
|
||||
public class OrderEmailListener {
|
||||
|
||||
private final EmailNotificationService emailNotificationService;
|
||||
private final InvoicePdfRenderingService invoicePdfRenderingService;
|
||||
private final OrderItemRepository orderItemRepository;
|
||||
private final QrBillService qrBillService;
|
||||
|
||||
@Value("${app.mail.admin.enabled:true}")
|
||||
private boolean adminMailEnabled;
|
||||
@@ -62,6 +70,20 @@ public class OrderEmailListener {
|
||||
}
|
||||
}
|
||||
|
||||
@Async
|
||||
@EventListener
|
||||
public void handlePaymentConfirmedEvent(PaymentConfirmedEvent event) {
|
||||
Order order = event.getOrder();
|
||||
Payment payment = event.getPayment();
|
||||
log.info("Processing PaymentConfirmedEvent for order id: {}", order.getId());
|
||||
|
||||
try {
|
||||
sendPaidInvoiceEmail(order, payment);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send paid invoice email for order id: {}", order.getId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendCustomerConfirmationEmail(Order order) {
|
||||
Map<String, Object> templateData = new HashMap<>();
|
||||
templateData.put("customerName", order.getCustomer().getFirstName());
|
||||
@@ -94,6 +116,34 @@ public class OrderEmailListener {
|
||||
);
|
||||
}
|
||||
|
||||
private void sendPaidInvoiceEmail(Order order, Payment payment) {
|
||||
Map<String, Object> 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()));
|
||||
|
||||
byte[] pdf = null;
|
||||
try {
|
||||
java.util.List<OrderItem> 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",
|
||||
"payment-confirmed",
|
||||
templateData,
|
||||
filename,
|
||||
pdf
|
||||
);
|
||||
}
|
||||
|
||||
private void sendAdminNotificationEmail(Order order) {
|
||||
Map<String, Object> templateData = new HashMap<>();
|
||||
templateData.put("customerName", order.getCustomer().getFirstName() + " " + order.getCustomer().getLastName());
|
||||
|
||||
@@ -8,10 +8,17 @@ import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.stream.Collectors;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import com.printcalculator.entity.Order;
|
||||
import com.printcalculator.entity.OrderItem;
|
||||
import com.printcalculator.entity.Payment;
|
||||
|
||||
@Service
|
||||
public class InvoicePdfRenderingService {
|
||||
|
||||
@@ -45,4 +52,92 @@ public class InvoicePdfRenderingService {
|
||||
throw new IllegalStateException("PDF invoice generation failed", pdfGenerationException);
|
||||
}
|
||||
}
|
||||
|
||||
public byte[] generateDocumentPdf(Order order, List<OrderItem> items, boolean isConfirmation, QrBillService qrBillService, Payment payment) {
|
||||
Map<String, Object> vars = new HashMap<>();
|
||||
vars.put("isConfirmation", isConfirmation);
|
||||
vars.put("sellerDisplayName", "3D Fab Küng Caletti");
|
||||
vars.put("sellerAddressLine1", "Joe Küng e Matteo Caletti");
|
||||
vars.put("sellerAddressLine2", "Sede Bienne, Svizzera");
|
||||
vars.put("sellerEmail", "info@3dfab.ch");
|
||||
|
||||
String displayOrderNumber = order.getOrderNumber() != null && !order.getOrderNumber().isBlank()
|
||||
? order.getOrderNumber()
|
||||
: order.getId().toString();
|
||||
|
||||
vars.put("invoiceNumber", "INV-" + displayOrderNumber.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 = order.getBillingCustomerType().equals("BUSINESS")
|
||||
? order.getBillingCompanyName()
|
||||
: order.getBillingFirstName() + " " + order.getBillingLastName();
|
||||
vars.put("buyerDisplayName", buyerName);
|
||||
vars.put("buyerAddressLine1", order.getBillingAddressLine1());
|
||||
vars.put("buyerAddressLine2", order.getBillingZip() + " " + order.getBillingCity() + ", " + order.getBillingCountryCode());
|
||||
|
||||
// Setup Shipping Info
|
||||
if (order.getShippingAddressLine1() != null && !order.getShippingAddressLine1().isBlank()) {
|
||||
String shippingName = order.getShippingCompanyName() != null && !order.getShippingCompanyName().isBlank()
|
||||
? order.getShippingCompanyName()
|
||||
: order.getShippingFirstName() + " " + order.getShippingLastName();
|
||||
vars.put("shippingDisplayName", shippingName);
|
||||
vars.put("shippingAddressLine1", order.getShippingAddressLine1());
|
||||
vars.put("shippingAddressLine2", order.getShippingZip() + " " + order.getShippingCity() + ", " + order.getShippingCountryCode());
|
||||
}
|
||||
|
||||
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", isConfirmation ? "Pagamento entro 7 giorni via Bonifico o TWINT. Grazie." : "Pagato. Grazie per l'acquisto.");
|
||||
|
||||
String paymentMethodText = "QR / Bonifico oppure TWINT";
|
||||
if (payment != null && payment.getMethod() != null) {
|
||||
paymentMethodText = switch (payment.getMethod().toUpperCase()) {
|
||||
case "TWINT" -> "TWINT";
|
||||
case "BANK_TRANSFER", "BONIFICO" -> "Bonifico Bancario";
|
||||
case "QR_BILL", "QR" -> "QR Bill";
|
||||
case "CASH" -> "Contanti";
|
||||
default -> payment.getMethod();
|
||||
};
|
||||
}
|
||||
vars.put("paymentMethodText", paymentMethodText);
|
||||
|
||||
String qrBillSvg = null;
|
||||
if (isConfirmation) {
|
||||
qrBillSvg = new String(qrBillService.generateQrBillSvg(order), java.nio.charset.StandardCharsets.UTF_8);
|
||||
|
||||
if (qrBillSvg.contains("<?xml")) {
|
||||
int svgStartIndex = qrBillSvg.indexOf("<svg");
|
||||
if (svgStartIndex != -1) {
|
||||
qrBillSvg = qrBillSvg.substring(svgStartIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return generateInvoicePdfBytesFromTemplate(vars, qrBillSvg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ 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;
|
||||
import com.printcalculator.repository.QuoteLineItemRepository;
|
||||
import com.printcalculator.repository.QuoteSessionRepository;
|
||||
import com.printcalculator.repository.PricingPolicyRepository;
|
||||
import com.printcalculator.service.QuoteCalculator;
|
||||
import com.printcalculator.event.OrderCreatedEvent;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -332,6 +332,7 @@ public class OrderService {
|
||||
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
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.printcalculator.service;
|
||||
import com.printcalculator.entity.Order;
|
||||
import com.printcalculator.entity.Payment;
|
||||
import com.printcalculator.event.PaymentReportedEvent;
|
||||
import com.printcalculator.event.PaymentConfirmedEvent;
|
||||
import com.printcalculator.repository.OrderRepository;
|
||||
import com.printcalculator.repository.PaymentRepository;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
@@ -73,4 +74,28 @@ public class PaymentService {
|
||||
|
||||
return payment;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Payment confirmPayment(UUID orderId, String method) {
|
||||
Order order = orderRepo.findById(orderId)
|
||||
.orElseThrow(() -> new RuntimeException("Order not found with id " + orderId));
|
||||
|
||||
Payment payment = paymentRepo.findByOrder_Id(orderId)
|
||||
.orElseGet(() -> getOrCreatePaymentForOrder(order, method != null ? method : "OTHER"));
|
||||
|
||||
payment.setStatus("COMPLETED");
|
||||
if (method != null && !method.isBlank()) {
|
||||
payment.setMethod(method.toUpperCase());
|
||||
}
|
||||
payment.setReceivedAt(OffsetDateTime.now());
|
||||
payment = paymentRepo.save(payment);
|
||||
|
||||
order.setStatus("IN_PRODUCTION");
|
||||
order.setPaidAt(OffsetDateTime.now());
|
||||
orderRepo.save(order);
|
||||
|
||||
eventPublisher.publishEvent(new PaymentConfirmedEvent(this, order, payment));
|
||||
|
||||
return payment;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public class QrBillService {
|
||||
// Creditor (Merchant)
|
||||
bill.setAccount("CH7409000000154821581"); // TODO: Configurable IBAN
|
||||
bill.setCreditor(createAddress(
|
||||
"Küng, Joe",
|
||||
"Joe Küng",
|
||||
"Via G. Pioda 29a",
|
||||
"6710",
|
||||
"Biasca",
|
||||
@@ -49,10 +49,7 @@ public class QrBillService {
|
||||
bill.setAmount(order.getTotalChf());
|
||||
bill.setCurrency("CHF");
|
||||
|
||||
// Reference
|
||||
// bill.setReference(QRBill.createCreditorReference("...")); // If using QRR
|
||||
String orderRef = order.getOrderNumber() != null ? order.getOrderNumber() : order.getId().toString();
|
||||
bill.setUnstructuredMessage("Order " + orderRef);
|
||||
bill.setUnstructuredMessage(order.getId().toString());
|
||||
|
||||
return bill;
|
||||
}
|
||||
|
||||
@@ -14,4 +14,16 @@ public interface EmailNotificationService {
|
||||
*/
|
||||
void sendEmail(String to, String subject, String templateName, Map<String, Object> contextData);
|
||||
|
||||
/**
|
||||
* Sends an HTML email using a Thymeleaf template, with an optional attachment.
|
||||
*
|
||||
* @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.
|
||||
* @param attachmentName The name for the attachment file.
|
||||
* @param attachmentData The raw bytes of the attachment.
|
||||
*/
|
||||
void sendEmailWithAttachment(String to, String subject, String templateName, Map<String, Object> contextData, String attachmentName, byte[] attachmentData);
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thymeleaf.TemplateEngine;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@@ -29,6 +30,11 @@ public class SmtpEmailNotificationService implements EmailNotificationService {
|
||||
|
||||
@Override
|
||||
public void sendEmail(String to, String subject, String templateName, Map<String, Object> contextData) {
|
||||
sendEmailWithAttachment(to, subject, templateName, contextData, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendEmailWithAttachment(String to, String subject, String templateName, Map<String, Object> contextData, String attachmentName, byte[] attachmentData) {
|
||||
if (!mailEnabled) {
|
||||
log.info("Email sending disabled (app.mail.enabled=false). Skipping email to {}", to);
|
||||
return;
|
||||
@@ -49,6 +55,10 @@ public class SmtpEmailNotificationService implements EmailNotificationService {
|
||||
helper.setSubject(subject);
|
||||
helper.setText(process, true); // true indicates HTML format
|
||||
|
||||
if (attachmentName != null && attachmentData != null) {
|
||||
helper.addAttachment(attachmentName, new ByteArrayResource(attachmentData));
|
||||
}
|
||||
|
||||
emailSender.send(mimeMessage);
|
||||
log.info("Email successfully sent to {}", to);
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
.invoice-page {
|
||||
page: invoice;
|
||||
width: 100%;
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
/* Top Header Layout */
|
||||
@@ -48,17 +49,17 @@
|
||||
|
||||
.logo-block {
|
||||
width: 33%;
|
||||
font-size: 20pt;
|
||||
font-size: 24pt;
|
||||
font-weight: bold;
|
||||
color: #005eb8; /* Brand blue similar to reference */
|
||||
letter-spacing: -0.5px;
|
||||
}
|
||||
|
||||
.logo-d {
|
||||
font-style: italic;
|
||||
color: #d22630;
|
||||
font-size: 22pt;
|
||||
margin-right: 2px;
|
||||
.logo-3d {
|
||||
color: #111827; /* Dark black/blue */
|
||||
}
|
||||
|
||||
.logo-fab {
|
||||
color: #eab308; /* Yellow/Gold */
|
||||
}
|
||||
|
||||
.seller-block {
|
||||
@@ -231,17 +232,26 @@
|
||||
/* QR Page */
|
||||
.qr-only-page {
|
||||
page: qrpage;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: 297mm;
|
||||
background: #fff;
|
||||
page-break-before: always;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
.qr-only-layout {
|
||||
width: 100%;
|
||||
height: 297mm;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.qr-only-layout td {
|
||||
vertical-align: bottom;
|
||||
/* Keep the QR slip at page bottom, but with a safer print margin. */
|
||||
padding: 0 0 1mm 0;
|
||||
}
|
||||
|
||||
.qr-bill-bottom {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 105mm;
|
||||
overflow: hidden;
|
||||
@@ -262,7 +272,8 @@
|
||||
<table class="header-layout">
|
||||
<tr>
|
||||
<td class="logo-block">
|
||||
3D-fab.ch
|
||||
<span class="logo-3d">3D</span> <span class="logo-fab">fab</span>
|
||||
<div style="font-size: 14pt; font-weight: normal; margin-top: 4px; color: #111827;">Küng Caletti</div>
|
||||
</td>
|
||||
<td class="seller-block">
|
||||
<div th:text="${sellerDisplayName}">3D Fab Switzerland</div>
|
||||
@@ -277,7 +288,10 @@
|
||||
|
||||
<!-- Document Title -->
|
||||
<div class="doc-title">
|
||||
Conferma dell'ordine <span th:text="${invoiceNumber}">141052743</span>
|
||||
<span th:if="${isConfirmation}">Conferma dell'ordine</span>
|
||||
<span th:unless="${isConfirmation}">Fattura</span>
|
||||
<span th:text="${invoiceNumber}">141052743</span>
|
||||
<span th:unless="${isConfirmation}" style="color: #2e7d32; font-weight: bold; font-size: 18pt; padding-left: 15px;">PAGATO</span>
|
||||
</div>
|
||||
|
||||
<!-- Details block (Meta and Customer) -->
|
||||
@@ -299,7 +313,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="meta-label">Metodo di pagamento</td>
|
||||
<td class="meta-value">QR / Bonifico oppure TWINT</td>
|
||||
<td class="meta-value" th:text="${paymentMethodText}">QR / Bonifico oppure TWINT</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="meta-label">Valuta</td>
|
||||
@@ -308,10 +322,19 @@
|
||||
</table>
|
||||
</td>
|
||||
<td class="customer-container">
|
||||
<div style="font-weight: bold; margin-bottom: 2mm;">Indirizzo di fatturazione:</div>
|
||||
<div th:text="${buyerDisplayName}">Joe Küng</div>
|
||||
<div th:text="${buyerAddressLine1}">Via G.Pioda, 29a</div>
|
||||
<div th:text="${buyerAddressLine2}">6710 biasca</div>
|
||||
<div>Svizzera</div>
|
||||
<br/>
|
||||
<div th:if="${shippingDisplayName != null}">
|
||||
<div style="font-weight: bold; margin-bottom: 2mm;">Indirizzo di spedizione:</div>
|
||||
<div th:text="${shippingDisplayName}">Joe Küng</div>
|
||||
<div th:text="${shippingAddressLine1}">Via G.Pioda, 29a</div>
|
||||
<div th:text="${shippingAddressLine2}">6710 biasca</div>
|
||||
<div>Svizzera</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -346,10 +369,14 @@
|
||||
<td class="totals-label">Totale di tutte le consegne e di tutti i servizi CHF</td>
|
||||
<td class="totals-value" th:text="${grandTotalFormatted}">1'094.90</td>
|
||||
</tr>
|
||||
<tr class="no-border">
|
||||
<tr class="no-border" th:if="${isConfirmation}">
|
||||
<td class="totals-label">Importo dovuto</td>
|
||||
<td class="totals-value" th:text="${grandTotalFormatted}">1'094.90</td>
|
||||
</tr>
|
||||
<tr class="no-border" th:unless="${isConfirmation}">
|
||||
<td class="totals-label">Importo dovuto</td>
|
||||
<td class="totals-value">CHF 0.00</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- Footer Notes -->
|
||||
@@ -372,8 +399,14 @@
|
||||
|
||||
<!-- QR Bill Page (only renders if QR data is passed) -->
|
||||
<div class="qr-only-page" th:if="${qrBillSvg != null}">
|
||||
<table class="qr-only-layout">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="qr-bill-bottom" th:utext="${qrBillSvg}">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -170,6 +170,16 @@ export class QuoteEstimatorService {
|
||||
});
|
||||
}
|
||||
|
||||
getOrderConfirmation(orderId: string): Observable<Blob> {
|
||||
const headers: any = {};
|
||||
// @ts-ignore
|
||||
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
||||
return this.http.get(`${environment.apiUrl}/api/orders/${orderId}/confirmation`, {
|
||||
headers,
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
getTwintPayment(orderId: string): Observable<any> {
|
||||
const headers: any = {};
|
||||
// @ts-ignore
|
||||
|
||||
@@ -87,18 +87,15 @@ align-items: center;" (click)="openTwintPayment()">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="payment-details fade-in" *ngIf="selectedPaymentMethod === 'bill'">
|
||||
<div class="payment-details fade-in text-center" *ngIf="selectedPaymentMethod === 'bill'">
|
||||
<div class="details-header">
|
||||
<h4>{{ 'PAYMENT.BANK_TITLE' | translate }}</h4>
|
||||
</div>
|
||||
<div class="bank-details">
|
||||
<p><strong>{{ 'PAYMENT.BANK_OWNER' | translate }}:</strong> Küng, Joe</p>
|
||||
<p><strong>{{ 'PAYMENT.BANK_IBAN' | translate }}:</strong> CH74 0900 0000 1548 2158 1</p>
|
||||
<p><strong>{{ 'PAYMENT.BANK_REF' | translate }}:</strong> {{ getDisplayOrderNumber(o) }}</p>
|
||||
<p class="billing-hint">{{ 'PAYMENT.BILLING_INFO_HINT' | translate }}</p>
|
||||
|
||||
<br>
|
||||
<div class="qr-bill-actions">
|
||||
<app-button (click)="downloadInvoice()">
|
||||
<app-button (click)="downloadQrInvoice()">
|
||||
{{ 'PAYMENT.DOWNLOAD_QR' | translate }}
|
||||
</app-button>
|
||||
</div>
|
||||
@@ -138,6 +135,7 @@ align-items: center;" (click)="openTwintPayment()">
|
||||
<span>{{ o.totalChf | currency:'CHF' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</app-card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,21 +58,21 @@ export class OrderComponent implements OnInit {
|
||||
this.selectedPaymentMethod = method;
|
||||
}
|
||||
|
||||
downloadInvoice() {
|
||||
downloadQrInvoice() {
|
||||
const orderId = this.orderId;
|
||||
if (!orderId) return;
|
||||
this.quoteService.getOrderInvoice(orderId).subscribe({
|
||||
this.quoteService.getOrderConfirmation(orderId).subscribe({
|
||||
next: (blob) => {
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
const fallbackOrderNumber = this.extractOrderNumber(orderId);
|
||||
const orderNumber = this.order()?.orderNumber ?? fallbackOrderNumber;
|
||||
a.download = `invoice-${orderNumber}.pdf`;
|
||||
a.download = `qr-invoice-${orderNumber}.pdf`;
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
},
|
||||
error: (err) => console.error('Failed to download invoice', err)
|
||||
error: (err) => console.error('Failed to download QR invoice', err)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -199,6 +199,7 @@
|
||||
"BANK_REF": "Reference",
|
||||
"BILLING_INFO_HINT": "Add the same information used in billing.",
|
||||
"DOWNLOAD_QR": "Download QR-Invoice (PDF)",
|
||||
"DOWNLOAD_CONFIRMATION": "Download Confirmation (PDF)",
|
||||
"CONFIRM": "I have completed the payment",
|
||||
"SUMMARY_TITLE": "Order Summary",
|
||||
"SUBTOTAL": "Subtotal",
|
||||
|
||||
@@ -271,6 +271,7 @@
|
||||
"BANK_REF": "Riferimento",
|
||||
"BILLING_INFO_HINT": "Abbiamo compilato i campi per te, per favore non modificare il motivo del pagamento",
|
||||
"DOWNLOAD_QR": "Scarica QR-Fattura (PDF)",
|
||||
"DOWNLOAD_CONFIRMATION": "Scarica Conferma (PDF)",
|
||||
"CONFIRM": "Ho completato il pagamento",
|
||||
"SUMMARY_TITLE": "Riepilogo Ordine",
|
||||
"SUBTOTAL": "Subtotale",
|
||||
|
||||
Reference in New Issue
Block a user