feat(back-end - front end): improvements in email and invoice rendering
All checks were successful
Build, Test and Deploy / test-backend (push) Successful in 37s
Build, Test and Deploy / build-and-push (push) Successful in 45s
Build, Test and Deploy / deploy (push) Successful in 10s

This commit is contained in:
2026-02-25 16:35:58 +01:00
parent fecb394272
commit c58d674a70
15 changed files with 317 additions and 102 deletions

View File

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

View File

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

View File

@@ -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());

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}">
<div class="qr-bill-bottom" th:utext="${qrBillSvg}">
</div>
<table class="qr-only-layout">
<tr>
<td>
<div class="qr-bill-bottom" th:utext="${qrBillSvg}">
</div>
</td>
</tr>
</table>
</div>
</body>

View File

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

View File

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

View File

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

View File

@@ -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",

View File

@@ -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",