feat(back-end and front-end): back-office

This commit is contained in:
2026-02-27 15:07:32 +01:00
parent 65e1ee3be6
commit 949770a741
38 changed files with 2558 additions and 345 deletions

View File

@@ -1,156 +0,0 @@
package com.printcalculator.controller;
import com.printcalculator.dto.AddressDto;
import com.printcalculator.dto.OrderDto;
import com.printcalculator.dto.OrderItemDto;
import com.printcalculator.entity.Order;
import com.printcalculator.entity.OrderItem;
import com.printcalculator.repository.OrderItemRepository;
import com.printcalculator.repository.OrderRepository;
import com.printcalculator.repository.PaymentRepository;
import com.printcalculator.service.PaymentService;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static org.springframework.http.HttpStatus.NOT_FOUND;
@RestController
@RequestMapping("/api/admin/orders")
public class AdminOrderController {
private final OrderRepository orderRepo;
private final OrderItemRepository orderItemRepo;
private final PaymentRepository paymentRepo;
private final PaymentService paymentService;
public AdminOrderController(
OrderRepository orderRepo,
OrderItemRepository orderItemRepo,
PaymentRepository paymentRepo,
PaymentService paymentService
) {
this.orderRepo = orderRepo;
this.orderItemRepo = orderItemRepo;
this.paymentRepo = paymentRepo;
this.paymentService = paymentService;
}
@GetMapping
public ResponseEntity<List<OrderDto>> listOrders() {
List<OrderDto> response = orderRepo.findAllByOrderByCreatedAtDesc()
.stream()
.map(this::toOrderDto)
.toList();
return ResponseEntity.ok(response);
}
@GetMapping("/{orderId}")
public ResponseEntity<OrderDto> getOrder(@PathVariable UUID orderId) {
return ResponseEntity.ok(toOrderDto(getOrderOrThrow(orderId)));
}
@PostMapping("/{orderId}/payments/confirm")
@Transactional
public ResponseEntity<OrderDto> confirmPayment(
@PathVariable UUID orderId,
@RequestBody(required = false) Map<String, String> payload
) {
getOrderOrThrow(orderId);
String method = payload != null ? payload.get("method") : null;
paymentService.confirmPayment(orderId, method);
return ResponseEntity.ok(toOrderDto(getOrderOrThrow(orderId)));
}
private Order getOrderOrThrow(UUID orderId) {
return orderRepo.findById(orderId)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Order not found"));
}
private OrderDto toOrderDto(Order order) {
List<OrderItem> items = orderItemRepo.findByOrder_Id(order.getId());
OrderDto dto = new OrderDto();
dto.setId(order.getId());
dto.setOrderNumber(getDisplayOrderNumber(order));
dto.setStatus(order.getStatus());
paymentRepo.findByOrder_Id(order.getId()).ifPresent(p -> {
dto.setPaymentStatus(p.getStatus());
dto.setPaymentMethod(p.getMethod());
});
dto.setCustomerEmail(order.getCustomerEmail());
dto.setCustomerPhone(order.getCustomerPhone());
dto.setPreferredLanguage(order.getPreferredLanguage());
dto.setBillingCustomerType(order.getBillingCustomerType());
dto.setCurrency(order.getCurrency());
dto.setSetupCostChf(order.getSetupCostChf());
dto.setShippingCostChf(order.getShippingCostChf());
dto.setDiscountChf(order.getDiscountChf());
dto.setSubtotalChf(order.getSubtotalChf());
dto.setTotalChf(order.getTotalChf());
dto.setCreatedAt(order.getCreatedAt());
dto.setShippingSameAsBilling(order.getShippingSameAsBilling());
AddressDto billing = new AddressDto();
billing.setFirstName(order.getBillingFirstName());
billing.setLastName(order.getBillingLastName());
billing.setCompanyName(order.getBillingCompanyName());
billing.setContactPerson(order.getBillingContactPerson());
billing.setAddressLine1(order.getBillingAddressLine1());
billing.setAddressLine2(order.getBillingAddressLine2());
billing.setZip(order.getBillingZip());
billing.setCity(order.getBillingCity());
billing.setCountryCode(order.getBillingCountryCode());
dto.setBillingAddress(billing);
if (!Boolean.TRUE.equals(order.getShippingSameAsBilling())) {
AddressDto shipping = new AddressDto();
shipping.setFirstName(order.getShippingFirstName());
shipping.setLastName(order.getShippingLastName());
shipping.setCompanyName(order.getShippingCompanyName());
shipping.setContactPerson(order.getShippingContactPerson());
shipping.setAddressLine1(order.getShippingAddressLine1());
shipping.setAddressLine2(order.getShippingAddressLine2());
shipping.setZip(order.getShippingZip());
shipping.setCity(order.getShippingCity());
shipping.setCountryCode(order.getShippingCountryCode());
dto.setShippingAddress(shipping);
}
List<OrderItemDto> itemDtos = items.stream().map(i -> {
OrderItemDto idto = new OrderItemDto();
idto.setId(i.getId());
idto.setOriginalFilename(i.getOriginalFilename());
idto.setMaterialCode(i.getMaterialCode());
idto.setColorCode(i.getColorCode());
idto.setQuantity(i.getQuantity());
idto.setPrintTimeSeconds(i.getPrintTimeSeconds());
idto.setMaterialGrams(i.getMaterialGrams());
idto.setUnitPriceChf(i.getUnitPriceChf());
idto.setLineTotalChf(i.getLineTotalChf());
return idto;
}).toList();
dto.setItems(itemDtos);
return dto;
}
private String getDisplayOrderNumber(Order order) {
String orderNumber = order.getOrderNumber();
if (orderNumber != null && !orderNumber.isBlank()) {
return orderNumber;
}
return order.getId() != null ? order.getId().toString() : "unknown";
}
}

View File

@@ -1,7 +1,9 @@
package com.printcalculator.controller;
package com.printcalculator.controller.admin;
import com.printcalculator.dto.AdminLoginRequest;
import com.printcalculator.security.AdminLoginThrottleService;
import com.printcalculator.security.AdminSessionService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import org.springframework.http.HttpHeaders;
@@ -13,26 +15,52 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.OptionalLong;
@RestController
@RequestMapping("/api/admin/auth")
public class AdminAuthController {
private final AdminSessionService adminSessionService;
private final AdminLoginThrottleService adminLoginThrottleService;
public AdminAuthController(AdminSessionService adminSessionService) {
public AdminAuthController(
AdminSessionService adminSessionService,
AdminLoginThrottleService adminLoginThrottleService
) {
this.adminSessionService = adminSessionService;
this.adminLoginThrottleService = adminLoginThrottleService;
}
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(
@Valid @RequestBody AdminLoginRequest request,
HttpServletRequest httpRequest,
HttpServletResponse response
) {
if (!adminSessionService.isPasswordValid(request.getPassword())) {
return ResponseEntity.status(401).body(Map.of("authenticated", false));
String clientKey = adminLoginThrottleService.resolveClientKey(httpRequest);
OptionalLong remainingLock = adminLoginThrottleService.getRemainingLockSeconds(clientKey);
if (remainingLock.isPresent()) {
long retryAfter = remainingLock.getAsLong();
return ResponseEntity.status(429)
.header(HttpHeaders.RETRY_AFTER, String.valueOf(retryAfter))
.body(Map.of(
"authenticated", false,
"retryAfterSeconds", retryAfter
));
}
if (!adminSessionService.isPasswordValid(request.getPassword())) {
long retryAfter = adminLoginThrottleService.registerFailure(clientKey);
return ResponseEntity.status(401)
.header(HttpHeaders.RETRY_AFTER, String.valueOf(retryAfter))
.body(Map.of(
"authenticated", false,
"retryAfterSeconds", retryAfter
));
}
adminLoginThrottleService.reset(clientKey);
String token = adminSessionService.createSessionToken();
response.addHeader(HttpHeaders.SET_COOKIE, adminSessionService.buildLoginCookie(token).toString());

View File

@@ -0,0 +1,142 @@
package com.printcalculator.controller.admin;
import com.printcalculator.dto.AdminContactRequestDto;
import com.printcalculator.dto.AdminFilamentStockDto;
import com.printcalculator.dto.AdminQuoteSessionDto;
import com.printcalculator.entity.CustomQuoteRequest;
import com.printcalculator.entity.FilamentVariant;
import com.printcalculator.entity.FilamentVariantStockKg;
import com.printcalculator.entity.QuoteSession;
import com.printcalculator.repository.CustomQuoteRequestRepository;
import com.printcalculator.repository.FilamentVariantRepository;
import com.printcalculator.repository.FilamentVariantStockKgRepository;
import com.printcalculator.repository.QuoteSessionRepository;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/admin")
@Transactional(readOnly = true)
public class AdminOperationsController {
private final FilamentVariantStockKgRepository filamentStockRepo;
private final FilamentVariantRepository filamentVariantRepo;
private final CustomQuoteRequestRepository customQuoteRequestRepo;
private final QuoteSessionRepository quoteSessionRepo;
public AdminOperationsController(
FilamentVariantStockKgRepository filamentStockRepo,
FilamentVariantRepository filamentVariantRepo,
CustomQuoteRequestRepository customQuoteRequestRepo,
QuoteSessionRepository quoteSessionRepo
) {
this.filamentStockRepo = filamentStockRepo;
this.filamentVariantRepo = filamentVariantRepo;
this.customQuoteRequestRepo = customQuoteRequestRepo;
this.quoteSessionRepo = quoteSessionRepo;
}
@GetMapping("/filament-stock")
public ResponseEntity<List<AdminFilamentStockDto>> getFilamentStock() {
List<FilamentVariantStockKg> stocks = filamentStockRepo.findAll(Sort.by(Sort.Direction.ASC, "stockKg"));
Set<Long> variantIds = stocks.stream()
.map(FilamentVariantStockKg::getFilamentVariantId)
.collect(Collectors.toSet());
Map<Long, FilamentVariant> variantsById;
if (variantIds.isEmpty()) {
variantsById = Collections.emptyMap();
} else {
variantsById = filamentVariantRepo.findAllById(variantIds).stream()
.collect(Collectors.toMap(FilamentVariant::getId, variant -> variant));
}
List<AdminFilamentStockDto> response = stocks.stream().map(stock -> {
FilamentVariant variant = variantsById.get(stock.getFilamentVariantId());
AdminFilamentStockDto dto = new AdminFilamentStockDto();
dto.setFilamentVariantId(stock.getFilamentVariantId());
dto.setStockSpools(stock.getStockSpools());
dto.setSpoolNetKg(stock.getSpoolNetKg());
dto.setStockKg(stock.getStockKg());
if (variant != null) {
dto.setMaterialCode(
variant.getFilamentMaterialType() != null
? variant.getFilamentMaterialType().getMaterialCode()
: "UNKNOWN"
);
dto.setVariantDisplayName(variant.getVariantDisplayName());
dto.setColorName(variant.getColorName());
dto.setActive(variant.getIsActive());
} else {
dto.setMaterialCode("UNKNOWN");
dto.setVariantDisplayName("Variant " + stock.getFilamentVariantId());
dto.setColorName("-");
dto.setActive(false);
}
return dto;
}).toList();
return ResponseEntity.ok(response);
}
@GetMapping("/contact-requests")
public ResponseEntity<List<AdminContactRequestDto>> getContactRequests() {
List<AdminContactRequestDto> response = customQuoteRequestRepo.findAll(
Sort.by(Sort.Direction.DESC, "createdAt")
)
.stream()
.map(this::toContactRequestDto)
.toList();
return ResponseEntity.ok(response);
}
@GetMapping("/sessions")
public ResponseEntity<List<AdminQuoteSessionDto>> getQuoteSessions() {
List<AdminQuoteSessionDto> response = quoteSessionRepo.findAll(
Sort.by(Sort.Direction.DESC, "createdAt")
)
.stream()
.map(this::toQuoteSessionDto)
.toList();
return ResponseEntity.ok(response);
}
private AdminContactRequestDto toContactRequestDto(CustomQuoteRequest request) {
AdminContactRequestDto dto = new AdminContactRequestDto();
dto.setId(request.getId());
dto.setRequestType(request.getRequestType());
dto.setCustomerType(request.getCustomerType());
dto.setEmail(request.getEmail());
dto.setPhone(request.getPhone());
dto.setName(request.getName());
dto.setCompanyName(request.getCompanyName());
dto.setStatus(request.getStatus());
dto.setCreatedAt(request.getCreatedAt());
return dto;
}
private AdminQuoteSessionDto toQuoteSessionDto(QuoteSession session) {
AdminQuoteSessionDto dto = new AdminQuoteSessionDto();
dto.setId(session.getId());
dto.setStatus(session.getStatus());
dto.setMaterialCode(session.getMaterialCode());
dto.setCreatedAt(session.getCreatedAt());
dto.setExpiresAt(session.getExpiresAt());
dto.setConvertedOrderId(session.getConvertedOrderId());
return dto;
}
}

View File

@@ -0,0 +1,301 @@
package com.printcalculator.controller.admin;
import com.printcalculator.dto.AddressDto;
import com.printcalculator.dto.AdminOrderStatusUpdateRequest;
import com.printcalculator.dto.OrderDto;
import com.printcalculator.dto.OrderItemDto;
import com.printcalculator.entity.Order;
import com.printcalculator.entity.OrderItem;
import com.printcalculator.entity.Payment;
import com.printcalculator.entity.QuoteSession;
import com.printcalculator.repository.OrderItemRepository;
import com.printcalculator.repository.OrderRepository;
import com.printcalculator.repository.PaymentRepository;
import com.printcalculator.service.InvoicePdfRenderingService;
import com.printcalculator.service.PaymentService;
import com.printcalculator.service.QrBillService;
import com.printcalculator.service.StorageService;
import org.springframework.core.io.Resource;
import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;
@RestController
@RequestMapping("/api/admin/orders")
@Transactional(readOnly = true)
public class AdminOrderController {
private static final List<String> ALLOWED_ORDER_STATUSES = List.of(
"PENDING_PAYMENT",
"PAID",
"IN_PRODUCTION",
"SHIPPED",
"COMPLETED",
"CANCELLED"
);
private final OrderRepository orderRepo;
private final OrderItemRepository orderItemRepo;
private final PaymentRepository paymentRepo;
private final PaymentService paymentService;
private final StorageService storageService;
private final InvoicePdfRenderingService invoiceService;
private final QrBillService qrBillService;
public AdminOrderController(
OrderRepository orderRepo,
OrderItemRepository orderItemRepo,
PaymentRepository paymentRepo,
PaymentService paymentService,
StorageService storageService,
InvoicePdfRenderingService invoiceService,
QrBillService qrBillService
) {
this.orderRepo = orderRepo;
this.orderItemRepo = orderItemRepo;
this.paymentRepo = paymentRepo;
this.paymentService = paymentService;
this.storageService = storageService;
this.invoiceService = invoiceService;
this.qrBillService = qrBillService;
}
@GetMapping
public ResponseEntity<List<OrderDto>> listOrders() {
List<OrderDto> response = orderRepo.findAllByOrderByCreatedAtDesc()
.stream()
.map(this::toOrderDto)
.toList();
return ResponseEntity.ok(response);
}
@GetMapping("/{orderId}")
public ResponseEntity<OrderDto> getOrder(@PathVariable UUID orderId) {
return ResponseEntity.ok(toOrderDto(getOrderOrThrow(orderId)));
}
@PostMapping("/{orderId}/payments/confirm")
@Transactional
public ResponseEntity<OrderDto> confirmPayment(
@PathVariable UUID orderId,
@RequestBody(required = false) Map<String, String> payload
) {
getOrderOrThrow(orderId);
String method = payload != null ? payload.get("method") : null;
paymentService.confirmPayment(orderId, method);
return ResponseEntity.ok(toOrderDto(getOrderOrThrow(orderId)));
}
@PostMapping("/{orderId}/status")
@Transactional
public ResponseEntity<OrderDto> updateOrderStatus(
@PathVariable UUID orderId,
@RequestBody AdminOrderStatusUpdateRequest payload
) {
if (payload == null || payload.getStatus() == null || payload.getStatus().isBlank()) {
throw new ResponseStatusException(org.springframework.http.HttpStatus.BAD_REQUEST, "Status is required");
}
Order order = getOrderOrThrow(orderId);
String normalizedStatus = payload.getStatus().trim().toUpperCase(Locale.ROOT);
if (!ALLOWED_ORDER_STATUSES.contains(normalizedStatus)) {
throw new ResponseStatusException(
BAD_REQUEST,
"Invalid order status. Allowed values: " + String.join(", ", ALLOWED_ORDER_STATUSES)
);
}
order.setStatus(normalizedStatus);
orderRepo.save(order);
return ResponseEntity.ok(toOrderDto(order));
}
@GetMapping("/{orderId}/items/{orderItemId}/file")
public ResponseEntity<Resource> downloadOrderItemFile(
@PathVariable UUID orderId,
@PathVariable UUID orderItemId
) {
OrderItem item = orderItemRepo.findById(orderItemId)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Order item not found"));
if (!item.getOrder().getId().equals(orderId)) {
throw new ResponseStatusException(NOT_FOUND, "Order item not found for order");
}
String relativePath = item.getStoredRelativePath();
if (relativePath == null || relativePath.isBlank() || "PENDING".equals(relativePath)) {
throw new ResponseStatusException(NOT_FOUND, "File not available");
}
try {
Resource resource = storageService.loadAsResource(Paths.get(relativePath));
MediaType contentType = MediaType.APPLICATION_OCTET_STREAM;
if (item.getMimeType() != null && !item.getMimeType().isBlank()) {
try {
contentType = MediaType.parseMediaType(item.getMimeType());
} catch (Exception ignored) {
contentType = MediaType.APPLICATION_OCTET_STREAM;
}
}
String filename = item.getOriginalFilename() != null && !item.getOriginalFilename().isBlank()
? item.getOriginalFilename()
: "order-item-" + orderItemId;
return ResponseEntity.ok()
.contentType(contentType)
.header(HttpHeaders.CONTENT_DISPOSITION, ContentDisposition.attachment()
.filename(filename, StandardCharsets.UTF_8)
.build()
.toString())
.body(resource);
} catch (Exception e) {
throw new ResponseStatusException(NOT_FOUND, "File not available");
}
}
@GetMapping("/{orderId}/documents/confirmation")
public ResponseEntity<byte[]> downloadOrderConfirmation(@PathVariable UUID orderId) {
return generateDocument(getOrderOrThrow(orderId), true);
}
@GetMapping("/{orderId}/documents/invoice")
public ResponseEntity<byte[]> downloadOrderInvoice(@PathVariable UUID orderId) {
return generateDocument(getOrderOrThrow(orderId), false);
}
private Order getOrderOrThrow(UUID orderId) {
return orderRepo.findById(orderId)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND, "Order not found"));
}
private OrderDto toOrderDto(Order order) {
List<OrderItem> items = orderItemRepo.findByOrder_Id(order.getId());
OrderDto dto = new OrderDto();
dto.setId(order.getId());
dto.setOrderNumber(getDisplayOrderNumber(order));
dto.setStatus(order.getStatus());
paymentRepo.findByOrder_Id(order.getId()).ifPresent(p -> {
dto.setPaymentStatus(p.getStatus());
dto.setPaymentMethod(p.getMethod());
});
dto.setCustomerEmail(order.getCustomerEmail());
dto.setCustomerPhone(order.getCustomerPhone());
dto.setPreferredLanguage(order.getPreferredLanguage());
dto.setBillingCustomerType(order.getBillingCustomerType());
dto.setCurrency(order.getCurrency());
dto.setSetupCostChf(order.getSetupCostChf());
dto.setShippingCostChf(order.getShippingCostChf());
dto.setDiscountChf(order.getDiscountChf());
dto.setSubtotalChf(order.getSubtotalChf());
dto.setTotalChf(order.getTotalChf());
dto.setCreatedAt(order.getCreatedAt());
dto.setShippingSameAsBilling(order.getShippingSameAsBilling());
QuoteSession sourceSession = order.getSourceQuoteSession();
if (sourceSession != null) {
dto.setPrintMaterialCode(sourceSession.getMaterialCode());
dto.setPrintNozzleDiameterMm(sourceSession.getNozzleDiameterMm());
dto.setPrintLayerHeightMm(sourceSession.getLayerHeightMm());
dto.setPrintInfillPattern(sourceSession.getInfillPattern());
dto.setPrintInfillPercent(sourceSession.getInfillPercent());
dto.setPrintSupportsEnabled(sourceSession.getSupportsEnabled());
}
AddressDto billing = new AddressDto();
billing.setFirstName(order.getBillingFirstName());
billing.setLastName(order.getBillingLastName());
billing.setCompanyName(order.getBillingCompanyName());
billing.setContactPerson(order.getBillingContactPerson());
billing.setAddressLine1(order.getBillingAddressLine1());
billing.setAddressLine2(order.getBillingAddressLine2());
billing.setZip(order.getBillingZip());
billing.setCity(order.getBillingCity());
billing.setCountryCode(order.getBillingCountryCode());
dto.setBillingAddress(billing);
if (!Boolean.TRUE.equals(order.getShippingSameAsBilling())) {
AddressDto shipping = new AddressDto();
shipping.setFirstName(order.getShippingFirstName());
shipping.setLastName(order.getShippingLastName());
shipping.setCompanyName(order.getShippingCompanyName());
shipping.setContactPerson(order.getShippingContactPerson());
shipping.setAddressLine1(order.getShippingAddressLine1());
shipping.setAddressLine2(order.getShippingAddressLine2());
shipping.setZip(order.getShippingZip());
shipping.setCity(order.getShippingCity());
shipping.setCountryCode(order.getShippingCountryCode());
dto.setShippingAddress(shipping);
}
List<OrderItemDto> itemDtos = items.stream().map(i -> {
OrderItemDto idto = new OrderItemDto();
idto.setId(i.getId());
idto.setOriginalFilename(i.getOriginalFilename());
idto.setMaterialCode(i.getMaterialCode());
idto.setColorCode(i.getColorCode());
idto.setQuantity(i.getQuantity());
idto.setPrintTimeSeconds(i.getPrintTimeSeconds());
idto.setMaterialGrams(i.getMaterialGrams());
idto.setUnitPriceChf(i.getUnitPriceChf());
idto.setLineTotalChf(i.getLineTotalChf());
return idto;
}).toList();
dto.setItems(itemDtos);
return dto;
}
private String getDisplayOrderNumber(Order order) {
String orderNumber = order.getOrderNumber();
if (orderNumber != null && !orderNumber.isBlank()) {
return orderNumber;
}
return order.getId() != null ? order.getId().toString() : "unknown";
}
private ResponseEntity<byte[]> generateDocument(Order order, boolean isConfirmation) {
String displayOrderNumber = getDisplayOrderNumber(order);
if (isConfirmation) {
String relativePath = "orders/" + order.getId() + "/documents/confirmation-" + displayOrderNumber + ".pdf";
try {
byte[] existingPdf = storageService.loadAsResource(Paths.get(relativePath)).getInputStream().readAllBytes();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"confirmation-" + displayOrderNumber + ".pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(existingPdf);
} catch (Exception ignored) {
// fallback to generated confirmation document
}
}
List<OrderItem> items = orderItemRepo.findByOrder_Id(order.getId());
Payment payment = paymentRepo.findByOrder_Id(order.getId()).orElse(null);
byte[] pdf = invoiceService.generateDocumentPdf(order, items, isConfirmation, qrBillService, payment);
String prefix = isConfirmation ? "confirmation-" : "invoice-";
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + prefix + displayOrderNumber + ".pdf\"")
.contentType(MediaType.APPLICATION_PDF)
.body(pdf);
}
}

View File

@@ -0,0 +1,88 @@
package com.printcalculator.dto;
import java.time.OffsetDateTime;
import java.util.UUID;
public class AdminContactRequestDto {
private UUID id;
private String requestType;
private String customerType;
private String email;
private String phone;
private String name;
private String companyName;
private String status;
private OffsetDateTime createdAt;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getRequestType() {
return requestType;
}
public void setRequestType(String requestType) {
this.requestType = requestType;
}
public String getCustomerType() {
return customerType;
}
public void setCustomerType(String customerType) {
this.customerType = customerType;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCompanyName() {
return companyName;
}
public void setCompanyName(String companyName) {
this.companyName = companyName;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
}

View File

@@ -0,0 +1,78 @@
package com.printcalculator.dto;
import java.math.BigDecimal;
public class AdminFilamentStockDto {
private Long filamentVariantId;
private String materialCode;
private String variantDisplayName;
private String colorName;
private BigDecimal stockSpools;
private BigDecimal spoolNetKg;
private BigDecimal stockKg;
private Boolean active;
public Long getFilamentVariantId() {
return filamentVariantId;
}
public void setFilamentVariantId(Long filamentVariantId) {
this.filamentVariantId = filamentVariantId;
}
public String getMaterialCode() {
return materialCode;
}
public void setMaterialCode(String materialCode) {
this.materialCode = materialCode;
}
public String getVariantDisplayName() {
return variantDisplayName;
}
public void setVariantDisplayName(String variantDisplayName) {
this.variantDisplayName = variantDisplayName;
}
public String getColorName() {
return colorName;
}
public void setColorName(String colorName) {
this.colorName = colorName;
}
public BigDecimal getStockSpools() {
return stockSpools;
}
public void setStockSpools(BigDecimal stockSpools) {
this.stockSpools = stockSpools;
}
public BigDecimal getSpoolNetKg() {
return spoolNetKg;
}
public void setSpoolNetKg(BigDecimal spoolNetKg) {
this.spoolNetKg = spoolNetKg;
}
public BigDecimal getStockKg() {
return stockKg;
}
public void setStockKg(BigDecimal stockKg) {
this.stockKg = stockKg;
}
public Boolean getActive() {
return active;
}
public void setActive(Boolean active) {
this.active = active;
}
}

View File

@@ -0,0 +1,13 @@
package com.printcalculator.dto;
public class AdminOrderStatusUpdateRequest {
private String status;
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}

View File

@@ -0,0 +1,61 @@
package com.printcalculator.dto;
import java.time.OffsetDateTime;
import java.util.UUID;
public class AdminQuoteSessionDto {
private UUID id;
private String status;
private String materialCode;
private OffsetDateTime createdAt;
private OffsetDateTime expiresAt;
private UUID convertedOrderId;
public UUID getId() {
return id;
}
public void setId(UUID id) {
this.id = id;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
public String getMaterialCode() {
return materialCode;
}
public void setMaterialCode(String materialCode) {
this.materialCode = materialCode;
}
public OffsetDateTime getCreatedAt() {
return createdAt;
}
public void setCreatedAt(OffsetDateTime createdAt) {
this.createdAt = createdAt;
}
public OffsetDateTime getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(OffsetDateTime expiresAt) {
this.expiresAt = expiresAt;
}
public UUID getConvertedOrderId() {
return convertedOrderId;
}
public void setConvertedOrderId(UUID convertedOrderId) {
this.convertedOrderId = convertedOrderId;
}
}

View File

@@ -25,6 +25,12 @@ public class OrderDto {
private BigDecimal subtotalChf;
private BigDecimal totalChf;
private OffsetDateTime createdAt;
private String printMaterialCode;
private BigDecimal printNozzleDiameterMm;
private BigDecimal printLayerHeightMm;
private String printInfillPattern;
private Integer printInfillPercent;
private Boolean printSupportsEnabled;
private List<OrderItemDto> items;
// Getters and Setters
@@ -85,6 +91,24 @@ public class OrderDto {
public OffsetDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(OffsetDateTime createdAt) { this.createdAt = createdAt; }
public String getPrintMaterialCode() { return printMaterialCode; }
public void setPrintMaterialCode(String printMaterialCode) { this.printMaterialCode = printMaterialCode; }
public BigDecimal getPrintNozzleDiameterMm() { return printNozzleDiameterMm; }
public void setPrintNozzleDiameterMm(BigDecimal printNozzleDiameterMm) { this.printNozzleDiameterMm = printNozzleDiameterMm; }
public BigDecimal getPrintLayerHeightMm() { return printLayerHeightMm; }
public void setPrintLayerHeightMm(BigDecimal printLayerHeightMm) { this.printLayerHeightMm = printLayerHeightMm; }
public String getPrintInfillPattern() { return printInfillPattern; }
public void setPrintInfillPattern(String printInfillPattern) { this.printInfillPattern = printInfillPattern; }
public Integer getPrintInfillPercent() { return printInfillPercent; }
public void setPrintInfillPercent(Integer printInfillPercent) { this.printInfillPercent = printInfillPercent; }
public Boolean getPrintSupportsEnabled() { return printSupportsEnabled; }
public void setPrintSupportsEnabled(Boolean printSupportsEnabled) { this.printSupportsEnabled = printSupportsEnabled; }
public List<OrderItemDto> getItems() { return items; }
public void setItems(List<OrderItemDto> items) { this.items = items; }
}

View File

@@ -0,0 +1,91 @@
package com.printcalculator.security;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.OptionalLong;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class AdminLoginThrottleService {
private static final long BASE_DELAY_SECONDS = 2L;
private static final long MAX_DELAY_SECONDS = 3600L;
private final ConcurrentHashMap<String, LoginAttemptState> attemptsByClient = new ConcurrentHashMap<>();
public OptionalLong getRemainingLockSeconds(String clientKey) {
LoginAttemptState state = attemptsByClient.get(clientKey);
if (state == null) {
return OptionalLong.empty();
}
long now = Instant.now().getEpochSecond();
long remaining = state.blockedUntilEpochSeconds - now;
if (remaining <= 0) {
attemptsByClient.remove(clientKey, state);
return OptionalLong.empty();
}
return OptionalLong.of(remaining);
}
public long registerFailure(String clientKey) {
long now = Instant.now().getEpochSecond();
LoginAttemptState state = attemptsByClient.compute(clientKey, (key, current) -> {
int nextFailures = current == null ? 1 : current.failures + 1;
long delay = calculateDelaySeconds(nextFailures);
return new LoginAttemptState(nextFailures, now + delay);
});
return calculateDelaySeconds(state.failures);
}
public void reset(String clientKey) {
attemptsByClient.remove(clientKey);
}
public String resolveClientKey(HttpServletRequest request) {
String forwardedFor = request.getHeader("X-Forwarded-For");
if (forwardedFor != null && !forwardedFor.isBlank()) {
String[] parts = forwardedFor.split(",");
if (parts.length > 0 && !parts[0].trim().isEmpty()) {
return parts[0].trim();
}
}
String realIp = request.getHeader("X-Real-IP");
if (realIp != null && !realIp.isBlank()) {
return realIp.trim();
}
String remoteAddress = request.getRemoteAddr();
if (remoteAddress != null && !remoteAddress.isBlank()) {
return remoteAddress.trim();
}
return "unknown";
}
private long calculateDelaySeconds(int failures) {
long delay = BASE_DELAY_SECONDS;
for (int i = 1; i < failures; i++) {
if (delay >= MAX_DELAY_SECONDS) {
return MAX_DELAY_SECONDS;
}
delay *= 2;
}
return Math.min(delay, MAX_DELAY_SECONDS);
}
private static class LoginAttemptState {
private final int failures;
private final long blockedUntilEpochSeconds;
private LoginAttemptState(int failures, long blockedUntilEpochSeconds) {
this.failures = failures;
this.blockedUntilEpochSeconds = blockedUntilEpochSeconds;
}
}
}

View File

@@ -2,6 +2,6 @@ app.mail.enabled=false
app.mail.admin.enabled=false
# Admin back-office local test credentials
admin.password=local-admin-password
admin.password=ciaociao
admin.session.secret=local-admin-session-secret-0123456789abcdef0123456789
admin.session.ttl-minutes=480