feat/calculator-options #26
@@ -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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
if (sourcePath == null || !Files.exists(sourcePath)) {
|
||||||
|
throw new IllegalStateException("Source file not available for quote line item " + qItem.getId());
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
Path sourcePath = Paths.get(qItem.getStoredPath());
|
|
||||||
if (Files.exists(sourcePath)) {
|
|
||||||
storageService.store(sourcePath, Paths.get(relativePath));
|
storageService.store(sourcePath, Paths.get(relativePath));
|
||||||
oItem.setFileSizeBytes(Files.size(sourcePath));
|
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()) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user