dev #29

Merged
JoeKung merged 30 commits from dev into main 2026-03-09 09:58:45 +01:00
4 changed files with 130 additions and 17 deletions
Showing only changes of commit 71424f086e - Show all commits

View File

@@ -18,6 +18,7 @@ import com.printcalculator.service.payment.QrBillService;
import com.printcalculator.service.storage.StorageService; import com.printcalculator.service.storage.StorageService;
import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisher;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.ContentDisposition; import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -32,8 +33,11 @@ import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException; import org.springframework.web.server.ResponseStatusException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.InvalidPathException; import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
@@ -46,6 +50,7 @@ import static org.springframework.http.HttpStatus.NOT_FOUND;
@RequestMapping("/api/admin/orders") @RequestMapping("/api/admin/orders")
@Transactional(readOnly = true) @Transactional(readOnly = true)
public class AdminOrderController { public class AdminOrderController {
private static final Path QUOTE_STORAGE_ROOT = Paths.get("storage_quotes").toAbsolutePath().normalize();
private static final List<String> ALLOWED_ORDER_STATUSES = List.of( private static final List<String> ALLOWED_ORDER_STATUSES = List.of(
"PENDING_PAYMENT", "PENDING_PAYMENT",
"PAID", "PAID",
@@ -58,6 +63,7 @@ public class AdminOrderController {
private final OrderRepository orderRepo; private final OrderRepository orderRepo;
private final OrderItemRepository orderItemRepo; private final OrderItemRepository orderItemRepo;
private final PaymentRepository paymentRepo; private final PaymentRepository paymentRepo;
private final QuoteLineItemRepository quoteLineItemRepo;
private final PaymentService paymentService; private final PaymentService paymentService;
private final StorageService storageService; private final StorageService storageService;
private final InvoicePdfRenderingService invoiceService; private final InvoicePdfRenderingService invoiceService;
@@ -68,6 +74,7 @@ public class AdminOrderController {
OrderRepository orderRepo, OrderRepository orderRepo,
OrderItemRepository orderItemRepo, OrderItemRepository orderItemRepo,
PaymentRepository paymentRepo, PaymentRepository paymentRepo,
QuoteLineItemRepository quoteLineItemRepo,
PaymentService paymentService, PaymentService paymentService,
StorageService storageService, StorageService storageService,
InvoicePdfRenderingService invoiceService, InvoicePdfRenderingService invoiceService,
@@ -77,6 +84,7 @@ public class AdminOrderController {
this.orderRepo = orderRepo; this.orderRepo = orderRepo;
this.orderItemRepo = orderItemRepo; this.orderItemRepo = orderItemRepo;
this.paymentRepo = paymentRepo; this.paymentRepo = paymentRepo;
this.quoteLineItemRepo = quoteLineItemRepo;
this.paymentService = paymentService; this.paymentService = paymentService;
this.storageService = storageService; this.storageService = storageService;
this.invoiceService = invoiceService; this.invoiceService = invoiceService;
@@ -277,12 +285,6 @@ public class AdminOrderController {
idto.setOriginalFilename(i.getOriginalFilename()); idto.setOriginalFilename(i.getOriginalFilename());
idto.setMaterialCode(i.getMaterialCode()); idto.setMaterialCode(i.getMaterialCode());
idto.setColorCode(i.getColorCode()); idto.setColorCode(i.getColorCode());
idto.setQuality(i.getQuality());
idto.setNozzleDiameterMm(i.getNozzleDiameterMm());
idto.setLayerHeightMm(i.getLayerHeightMm());
idto.setInfillPercent(i.getInfillPercent());
idto.setInfillPattern(i.getInfillPattern());
idto.setSupportsEnabled(i.getSupportsEnabled());
idto.setQuantity(i.getQuantity()); idto.setQuantity(i.getQuantity());
idto.setPrintTimeSeconds(i.getPrintTimeSeconds()); idto.setPrintTimeSeconds(i.getPrintTimeSeconds());
idto.setMaterialGrams(i.getMaterialGrams()); idto.setMaterialGrams(i.getMaterialGrams());
@@ -345,6 +347,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) { private Path buildConfirmationPdfRelativePath(UUID orderId, String orderNumber) {
return Path.of("orders", orderId.toString(), "documents", "confirmation-" + orderNumber + ".pdf"); return Path.of("orders", orderId.toString(), "documents", "confirmation-" + orderNumber + ".pdf");
} }

View File

@@ -20,6 +20,7 @@ import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.time.OffsetDateTime; import java.time.OffsetDateTime;
@@ -27,6 +28,7 @@ import java.util.*;
@Service @Service
public class OrderService { public class OrderService {
private static final Path QUOTE_STORAGE_ROOT = Paths.get("storage_quotes").toAbsolutePath().normalize();
private final OrderRepository orderRepo; private final OrderRepository orderRepo;
private final OrderItemRepository orderItemRepo; private final OrderItemRepository orderItemRepo;
@@ -220,16 +222,15 @@ public class OrderService {
String relativePath = "orders/" + order.getId() + "/3d-files/" + oItem.getId() + "/" + storedFilename; String relativePath = "orders/" + order.getId() + "/3d-files/" + oItem.getId() + "/" + storedFilename;
oItem.setStoredRelativePath(relativePath); oItem.setStoredRelativePath(relativePath);
if (qItem.getStoredPath() != null) { Path sourcePath = resolveStoredQuotePath(qItem.getStoredPath(), session.getId());
try { if (sourcePath == null || !Files.exists(sourcePath)) {
Path sourcePath = Paths.get(qItem.getStoredPath()); throw new IllegalStateException("Source file not available for quote line item " + qItem.getId());
if (Files.exists(sourcePath)) { }
storageService.store(sourcePath, Paths.get(relativePath)); try {
oItem.setFileSizeBytes(Files.size(sourcePath)); storageService.store(sourcePath, Paths.get(relativePath));
} oItem.setFileSizeBytes(Files.size(sourcePath));
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); throw new RuntimeException("Failed to copy quote file for line item " + qItem.getId(), e);
}
} }
oItem = orderItemRepo.save(oItem); oItem = orderItemRepo.save(oItem);
@@ -301,6 +302,23 @@ public class OrderService {
return "stl"; 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) { private String getDisplayOrderNumber(Order order) {
String orderNumber = order.getOrderNumber(); String orderNumber = order.getOrderNumber();
if (orderNumber != null && !orderNumber.isBlank()) { if (orderNumber != null && !orderNumber.isBlank()) {

View File

@@ -27,7 +27,7 @@ clamav.port=${CLAMAV_PORT:3310}
clamav.enabled=${CLAMAV_ENABLED:false} clamav.enabled=${CLAMAV_ENABLED:false}
# TWINT Configuration # 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 # Mail Configuration
spring.mail.host=${MAIL_HOST:mail.infomaniak.com} spring.mail.host=${MAIL_HOST:mail.infomaniak.com}

View File

@@ -13,6 +13,7 @@ services:
- CLAMAV_HOST=${CLAMAV_HOST} - CLAMAV_HOST=${CLAMAV_HOST}
- CLAMAV_PORT=${CLAMAV_PORT} - CLAMAV_PORT=${CLAMAV_PORT}
- CLAMAV_ENABLED=${CLAMAV_ENABLED} - CLAMAV_ENABLED=${CLAMAV_ENABLED}
- TWINT_PAYMENT_URL=${TWINT_PAYMENT_URL:-}
- MAIL_HOST=${MAIL_HOST:-mail.infomaniak.com} - MAIL_HOST=${MAIL_HOST:-mail.infomaniak.com}
- MAIL_PORT=${MAIL_PORT:-587} - MAIL_PORT=${MAIL_PORT:-587}
- MAIL_USERNAME=${MAIL_USERNAME:-info@3d-fab.ch} - MAIL_USERNAME=${MAIL_USERNAME:-info@3d-fab.ch}