From 9facf05c1077a752a39de6f8ab48388669fed9f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Thu, 5 Mar 2026 15:44:03 +0100 Subject: [PATCH 1/2] fix(back-end): twint url --- backend/src/main/resources/application.properties | 2 +- docker-compose.deploy.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index db27cf5..0915c7e 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -27,7 +27,7 @@ clamav.port=${CLAMAV_PORT:3310} clamav.enabled=${CLAMAV_ENABLED:false} # TWINT Configuration -payment.twint.url=${TWINT_PAYMENT_URL:} +payment.twint.url=${TWINT_PAYMENT_URL:https://go.twint.ch/1/e/tw?tw=acq.gERQQytOTnyIMuQHUqn4hlxgciHE5X7nnqHnNSPAr2OF2K3uBlXJDr2n9JU3sgxa.} # Mail Configuration spring.mail.host=${MAIL_HOST:mail.infomaniak.com} diff --git a/docker-compose.deploy.yml b/docker-compose.deploy.yml index 1a141fe..a3db5b0 100644 --- a/docker-compose.deploy.yml +++ b/docker-compose.deploy.yml @@ -13,6 +13,7 @@ services: - CLAMAV_HOST=${CLAMAV_HOST} - CLAMAV_PORT=${CLAMAV_PORT} - CLAMAV_ENABLED=${CLAMAV_ENABLED} + - TWINT_PAYMENT_URL=${TWINT_PAYMENT_URL:-} - MAIL_HOST=${MAIL_HOST:-mail.infomaniak.com} - MAIL_PORT=${MAIL_PORT:-587} - MAIL_USERNAME=${MAIL_USERNAME:-info@3d-fab.ch} From 54b50028b121e5d59b91d291d73240ad7a0e936c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Thu, 5 Mar 2026 16:37:54 +0100 Subject: [PATCH 2/2] fix(back-end): path solver --- .../admin/AdminOrderController.java | 103 +++++++++++++++++- .../printcalculator/service/OrderService.java | 38 +++++-- 2 files changed, 130 insertions(+), 11 deletions(-) diff --git a/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java b/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java index 764940d..70aea94 100644 --- a/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java +++ b/backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java @@ -7,17 +7,20 @@ import com.printcalculator.dto.OrderItemDto; import com.printcalculator.entity.Order; import com.printcalculator.entity.OrderItem; import com.printcalculator.entity.Payment; +import com.printcalculator.entity.QuoteLineItem; import com.printcalculator.entity.QuoteSession; import com.printcalculator.event.OrderShippedEvent; import com.printcalculator.repository.OrderItemRepository; import com.printcalculator.repository.OrderRepository; import com.printcalculator.repository.PaymentRepository; +import com.printcalculator.repository.QuoteLineItemRepository; import com.printcalculator.service.InvoicePdfRenderingService; import com.printcalculator.service.PaymentService; import com.printcalculator.service.QrBillService; import com.printcalculator.service.StorageService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -32,9 +35,11 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.server.ResponseStatusException; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Comparator; import java.util.List; import java.util.Locale; import java.util.Map; @@ -47,6 +52,7 @@ import static org.springframework.http.HttpStatus.NOT_FOUND; @RequestMapping("/api/admin/orders") @Transactional(readOnly = true) public class AdminOrderController { + private static final Path QUOTE_STORAGE_ROOT = Paths.get("storage_quotes").toAbsolutePath().normalize(); private static final List ALLOWED_ORDER_STATUSES = List.of( "PENDING_PAYMENT", "PAID", @@ -59,6 +65,7 @@ public class AdminOrderController { private final OrderRepository orderRepo; private final OrderItemRepository orderItemRepo; private final PaymentRepository paymentRepo; + private final QuoteLineItemRepository quoteLineItemRepo; private final PaymentService paymentService; private final StorageService storageService; private final InvoicePdfRenderingService invoiceService; @@ -69,6 +76,7 @@ public class AdminOrderController { OrderRepository orderRepo, OrderItemRepository orderItemRepo, PaymentRepository paymentRepo, + QuoteLineItemRepository quoteLineItemRepo, PaymentService paymentService, StorageService storageService, InvoicePdfRenderingService invoiceService, @@ -78,6 +86,7 @@ public class AdminOrderController { this.orderRepo = orderRepo; this.orderItemRepo = orderItemRepo; this.paymentRepo = paymentRepo; + this.quoteLineItemRepo = quoteLineItemRepo; this.paymentService = paymentService; this.storageService = storageService; this.invoiceService = invoiceService; @@ -166,7 +175,7 @@ public class AdminOrderController { } try { - Resource resource = storageService.loadAsResource(safeRelativePath); + Resource resource = loadOrderItemResourceWithRecovery(item, safeRelativePath); MediaType contentType = MediaType.APPLICATION_OCTET_STREAM; if (item.getMimeType() != null && !item.getMimeType().isBlank()) { try { @@ -340,6 +349,98 @@ public class AdminOrderController { } } + private Resource loadOrderItemResourceWithRecovery(OrderItem item, Path safeRelativePath) { + try { + return storageService.loadAsResource(safeRelativePath); + } catch (Exception primaryFailure) { + Path sourceQuotePath = resolveFallbackQuoteItemPath(item); + if (sourceQuotePath == null) { + throw new ResponseStatusException(NOT_FOUND, "File not available"); + } + try { + storageService.store(sourceQuotePath, safeRelativePath); + return storageService.loadAsResource(safeRelativePath); + } catch (Exception copyFailure) { + try { + Resource quoteResource = new UrlResource(sourceQuotePath.toUri()); + if (quoteResource.exists() || quoteResource.isReadable()) { + return quoteResource; + } + } catch (Exception ignored) { + // fall through to 404 + } + throw new ResponseStatusException(NOT_FOUND, "File not available"); + } + } + } + + private Path resolveFallbackQuoteItemPath(OrderItem orderItem) { + Order order = orderItem.getOrder(); + QuoteSession sourceSession = order != null ? order.getSourceQuoteSession() : null; + UUID sourceSessionId = sourceSession != null ? sourceSession.getId() : null; + if (sourceSessionId == null) { + return null; + } + + String targetFilename = normalizeFilename(orderItem.getOriginalFilename()); + if (targetFilename == null) { + return null; + } + + return quoteLineItemRepo.findByQuoteSessionId(sourceSessionId).stream() + .filter(q -> targetFilename.equals(normalizeFilename(q.getOriginalFilename()))) + .sorted(Comparator.comparingInt((QuoteLineItem q) -> scoreQuoteMatch(orderItem, q)).reversed()) + .map(q -> resolveStoredQuotePath(q.getStoredPath(), sourceSessionId)) + .filter(path -> path != null && Files.exists(path)) + .findFirst() + .orElse(null); + } + + private int scoreQuoteMatch(OrderItem orderItem, QuoteLineItem quoteItem) { + int score = 0; + if (orderItem.getQuantity() != null && orderItem.getQuantity().equals(quoteItem.getQuantity())) { + score += 4; + } + if (orderItem.getPrintTimeSeconds() != null && orderItem.getPrintTimeSeconds().equals(quoteItem.getPrintTimeSeconds())) { + score += 3; + } + if (orderItem.getMaterialCode() != null + && quoteItem.getMaterialCode() != null + && orderItem.getMaterialCode().equalsIgnoreCase(quoteItem.getMaterialCode())) { + score += 3; + } + if (orderItem.getMaterialGrams() != null + && quoteItem.getMaterialGrams() != null + && orderItem.getMaterialGrams().compareTo(quoteItem.getMaterialGrams()) == 0) { + score += 2; + } + return score; + } + + private String normalizeFilename(String filename) { + if (filename == null || filename.isBlank()) { + return null; + } + return filename.trim(); + } + + private Path resolveStoredQuotePath(String storedPath, UUID expectedSessionId) { + if (storedPath == null || storedPath.isBlank()) { + return null; + } + try { + Path raw = Path.of(storedPath).normalize(); + Path resolved = raw.isAbsolute() ? raw : QUOTE_STORAGE_ROOT.resolve(raw).normalize(); + Path expectedSessionRoot = QUOTE_STORAGE_ROOT.resolve(expectedSessionId.toString()).normalize(); + if (!resolved.startsWith(expectedSessionRoot)) { + return null; + } + return resolved; + } catch (InvalidPathException e) { + return null; + } + } + private Path buildConfirmationPdfRelativePath(UUID orderId, String orderNumber) { return Path.of("orders", orderId.toString(), "documents", "confirmation-" + orderNumber + ".pdf"); } diff --git a/backend/src/main/java/com/printcalculator/service/OrderService.java b/backend/src/main/java/com/printcalculator/service/OrderService.java index 3a1f606..1ab620e 100644 --- a/backend/src/main/java/com/printcalculator/service/OrderService.java +++ b/backend/src/main/java/com/printcalculator/service/OrderService.java @@ -16,6 +16,7 @@ import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.nio.file.Paths; import java.time.OffsetDateTime; @@ -23,6 +24,7 @@ import java.util.*; @Service public class OrderService { + private static final Path QUOTE_STORAGE_ROOT = Paths.get("storage_quotes").toAbsolutePath().normalize(); private final OrderRepository orderRepo; private final OrderItemRepository orderItemRepo; @@ -210,16 +212,15 @@ public class OrderService { String relativePath = "orders/" + order.getId() + "/3d-files/" + oItem.getId() + "/" + storedFilename; oItem.setStoredRelativePath(relativePath); - if (qItem.getStoredPath() != null) { - try { - Path sourcePath = Paths.get(qItem.getStoredPath()); - if (Files.exists(sourcePath)) { - storageService.store(sourcePath, Paths.get(relativePath)); - oItem.setFileSizeBytes(Files.size(sourcePath)); - } - } catch (IOException e) { - e.printStackTrace(); - } + Path sourcePath = resolveStoredQuotePath(qItem.getStoredPath(), session.getId()); + if (sourcePath == null || !Files.exists(sourcePath)) { + throw new IllegalStateException("Source file not available for quote line item " + qItem.getId()); + } + try { + storageService.store(sourcePath, Paths.get(relativePath)); + oItem.setFileSizeBytes(Files.size(sourcePath)); + } catch (IOException e) { + throw new RuntimeException("Failed to copy quote file for line item " + qItem.getId(), e); } oItem = orderItemRepo.save(oItem); @@ -291,6 +292,23 @@ public class OrderService { return "stl"; } + private Path resolveStoredQuotePath(String storedPath, UUID expectedSessionId) { + if (storedPath == null || storedPath.isBlank()) { + return null; + } + try { + Path raw = Path.of(storedPath).normalize(); + Path resolved = raw.isAbsolute() ? raw : QUOTE_STORAGE_ROOT.resolve(raw).normalize(); + Path expectedSessionRoot = QUOTE_STORAGE_ROOT.resolve(expectedSessionId.toString()).normalize(); + if (!resolved.startsWith(expectedSessionRoot)) { + return null; + } + return resolved; + } catch (InvalidPathException e) { + return null; + } + } + private String getDisplayOrderNumber(Order order) { String orderNumber = order.getOrderNumber(); if (orderNumber != null && !orderNumber.isBlank()) {