Merge branch 'fix/twint' into feat/calculator-options
# Conflicts: # backend/src/main/java/com/printcalculator/controller/admin/AdminOrderController.java
This commit is contained in:
@@ -18,6 +18,7 @@ import com.printcalculator.service.payment.QrBillService;
|
||||
import com.printcalculator.service.storage.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,8 +33,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;
|
||||
@@ -46,6 +50,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<String> ALLOWED_ORDER_STATUSES = List.of(
|
||||
"PENDING_PAYMENT",
|
||||
"PAID",
|
||||
@@ -58,6 +63,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;
|
||||
@@ -68,6 +74,7 @@ public class AdminOrderController {
|
||||
OrderRepository orderRepo,
|
||||
OrderItemRepository orderItemRepo,
|
||||
PaymentRepository paymentRepo,
|
||||
QuoteLineItemRepository quoteLineItemRepo,
|
||||
PaymentService paymentService,
|
||||
StorageService storageService,
|
||||
InvoicePdfRenderingService invoiceService,
|
||||
@@ -77,6 +84,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;
|
||||
@@ -277,12 +285,6 @@ public class AdminOrderController {
|
||||
idto.setOriginalFilename(i.getOriginalFilename());
|
||||
idto.setMaterialCode(i.getMaterialCode());
|
||||
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.setPrintTimeSeconds(i.getPrintTimeSeconds());
|
||||
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) {
|
||||
return Path.of("orders", orderId.toString(), "documents", "confirmation-" + orderNumber + ".pdf");
|
||||
}
|
||||
|
||||
@@ -20,6 +20,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;
|
||||
@@ -27,6 +28,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;
|
||||
@@ -220,16 +222,15 @@ public class OrderService {
|
||||
String relativePath = "orders/" + order.getId() + "/3d-files/" + oItem.getId() + "/" + storedFilename;
|
||||
oItem.setStoredRelativePath(relativePath);
|
||||
|
||||
if (qItem.getStoredPath() != null) {
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
throw new RuntimeException("Failed to copy quote file for line item " + qItem.getId(), e);
|
||||
}
|
||||
|
||||
oItem = orderItemRepo.save(oItem);
|
||||
@@ -301,6 +302,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()) {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user