produzione 1 #9
5
.gitignore
vendored
5
.gitignore
vendored
@@ -41,3 +41,8 @@ target/
|
|||||||
build/
|
build/
|
||||||
.gradle/
|
.gradle/
|
||||||
.mvn/
|
.mvn/
|
||||||
|
|
||||||
|
./storage_orders
|
||||||
|
./storage_quotes
|
||||||
|
storage_orders
|
||||||
|
storage_quotes
|
||||||
@@ -5,6 +5,7 @@ import com.printcalculator.entity.*;
|
|||||||
import com.printcalculator.repository.*;
|
import com.printcalculator.repository.*;
|
||||||
import com.printcalculator.service.InvoicePdfRenderingService;
|
import com.printcalculator.service.InvoicePdfRenderingService;
|
||||||
import com.printcalculator.service.OrderService;
|
import com.printcalculator.service.OrderService;
|
||||||
|
import com.printcalculator.service.PaymentService;
|
||||||
import com.printcalculator.service.QrBillService;
|
import com.printcalculator.service.QrBillService;
|
||||||
import com.printcalculator.service.StorageService;
|
import com.printcalculator.service.StorageService;
|
||||||
import com.printcalculator.service.TwintPaymentService;
|
import com.printcalculator.service.TwintPaymentService;
|
||||||
@@ -43,6 +44,8 @@ public class OrderController {
|
|||||||
private final InvoicePdfRenderingService invoiceService;
|
private final InvoicePdfRenderingService invoiceService;
|
||||||
private final QrBillService qrBillService;
|
private final QrBillService qrBillService;
|
||||||
private final TwintPaymentService twintPaymentService;
|
private final TwintPaymentService twintPaymentService;
|
||||||
|
private final PaymentService paymentService;
|
||||||
|
private final PaymentRepository paymentRepo;
|
||||||
|
|
||||||
|
|
||||||
public OrderController(OrderService orderService,
|
public OrderController(OrderService orderService,
|
||||||
@@ -54,7 +57,9 @@ public class OrderController {
|
|||||||
StorageService storageService,
|
StorageService storageService,
|
||||||
InvoicePdfRenderingService invoiceService,
|
InvoicePdfRenderingService invoiceService,
|
||||||
QrBillService qrBillService,
|
QrBillService qrBillService,
|
||||||
TwintPaymentService twintPaymentService) {
|
TwintPaymentService twintPaymentService,
|
||||||
|
PaymentService paymentService,
|
||||||
|
PaymentRepository paymentRepo) {
|
||||||
this.orderService = orderService;
|
this.orderService = orderService;
|
||||||
this.orderRepo = orderRepo;
|
this.orderRepo = orderRepo;
|
||||||
this.orderItemRepo = orderItemRepo;
|
this.orderItemRepo = orderItemRepo;
|
||||||
@@ -65,6 +70,8 @@ public class OrderController {
|
|||||||
this.invoiceService = invoiceService;
|
this.invoiceService = invoiceService;
|
||||||
this.qrBillService = qrBillService;
|
this.qrBillService = qrBillService;
|
||||||
this.twintPaymentService = twintPaymentService;
|
this.twintPaymentService = twintPaymentService;
|
||||||
|
this.paymentService = paymentService;
|
||||||
|
this.paymentRepo = paymentRepo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -122,6 +129,17 @@ public class OrderController {
|
|||||||
.orElse(ResponseEntity.notFound().build());
|
.orElse(ResponseEntity.notFound().build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{orderId}/payments/report")
|
||||||
|
@Transactional
|
||||||
|
public ResponseEntity<OrderDto> reportPayment(
|
||||||
|
@PathVariable UUID orderId,
|
||||||
|
@RequestBody Map<String, String> payload
|
||||||
|
) {
|
||||||
|
String method = payload.get("method");
|
||||||
|
paymentService.reportPayment(orderId, method);
|
||||||
|
return getOrder(orderId);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/{orderId}/invoice")
|
@GetMapping("/{orderId}/invoice")
|
||||||
public ResponseEntity<byte[]> getInvoice(@PathVariable UUID orderId) {
|
public ResponseEntity<byte[]> getInvoice(@PathVariable UUID orderId) {
|
||||||
Order order = orderRepo.findById(orderId)
|
Order order = orderRepo.findById(orderId)
|
||||||
@@ -251,6 +269,12 @@ public class OrderController {
|
|||||||
dto.setId(order.getId());
|
dto.setId(order.getId());
|
||||||
dto.setOrderNumber(getDisplayOrderNumber(order));
|
dto.setOrderNumber(getDisplayOrderNumber(order));
|
||||||
dto.setStatus(order.getStatus());
|
dto.setStatus(order.getStatus());
|
||||||
|
|
||||||
|
paymentRepo.findByOrder_Id(order.getId()).ifPresent(p -> {
|
||||||
|
dto.setPaymentStatus(p.getStatus());
|
||||||
|
dto.setPaymentMethod(p.getMethod());
|
||||||
|
});
|
||||||
|
|
||||||
dto.setCustomerEmail(order.getCustomerEmail());
|
dto.setCustomerEmail(order.getCustomerEmail());
|
||||||
dto.setCustomerPhone(order.getCustomerPhone());
|
dto.setCustomerPhone(order.getCustomerPhone());
|
||||||
dto.setBillingCustomerType(order.getBillingCustomerType());
|
dto.setBillingCustomerType(order.getBillingCustomerType());
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ public class OrderDto {
|
|||||||
private UUID id;
|
private UUID id;
|
||||||
private String orderNumber;
|
private String orderNumber;
|
||||||
private String status;
|
private String status;
|
||||||
|
private String paymentStatus;
|
||||||
|
private String paymentMethod;
|
||||||
private String customerEmail;
|
private String customerEmail;
|
||||||
private String customerPhone;
|
private String customerPhone;
|
||||||
private String billingCustomerType;
|
private String billingCustomerType;
|
||||||
@@ -34,6 +36,12 @@ public class OrderDto {
|
|||||||
public String getStatus() { return status; }
|
public String getStatus() { return status; }
|
||||||
public void setStatus(String status) { this.status = status; }
|
public void setStatus(String status) { this.status = status; }
|
||||||
|
|
||||||
|
public String getPaymentStatus() { return paymentStatus; }
|
||||||
|
public void setPaymentStatus(String paymentStatus) { this.paymentStatus = paymentStatus; }
|
||||||
|
|
||||||
|
public String getPaymentMethod() { return paymentMethod; }
|
||||||
|
public void setPaymentMethod(String paymentMethod) { this.paymentMethod = paymentMethod; }
|
||||||
|
|
||||||
public String getCustomerEmail() { return customerEmail; }
|
public String getCustomerEmail() { return customerEmail; }
|
||||||
public void setCustomerEmail(String customerEmail) { this.customerEmail = customerEmail; }
|
public void setCustomerEmail(String customerEmail) { this.customerEmail = customerEmail; }
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ public class Payment {
|
|||||||
@Column(name = "initiated_at", nullable = false)
|
@Column(name = "initiated_at", nullable = false)
|
||||||
private OffsetDateTime initiatedAt;
|
private OffsetDateTime initiatedAt;
|
||||||
|
|
||||||
|
@Column(name = "reported_at")
|
||||||
|
private OffsetDateTime reportedAt;
|
||||||
|
|
||||||
@Column(name = "received_at")
|
@Column(name = "received_at")
|
||||||
private OffsetDateTime receivedAt;
|
private OffsetDateTime receivedAt;
|
||||||
|
|
||||||
@@ -135,6 +138,14 @@ public class Payment {
|
|||||||
this.initiatedAt = initiatedAt;
|
this.initiatedAt = initiatedAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public OffsetDateTime getReportedAt() {
|
||||||
|
return reportedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReportedAt(OffsetDateTime reportedAt) {
|
||||||
|
this.reportedAt = reportedAt;
|
||||||
|
}
|
||||||
|
|
||||||
public OffsetDateTime getReceivedAt() {
|
public OffsetDateTime getReceivedAt() {
|
||||||
return receivedAt;
|
return receivedAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.printcalculator.event;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.Order;
|
||||||
|
import com.printcalculator.entity.Payment;
|
||||||
|
import org.springframework.context.ApplicationEvent;
|
||||||
|
|
||||||
|
public class PaymentReportedEvent extends ApplicationEvent {
|
||||||
|
|
||||||
|
private final Order order;
|
||||||
|
private final Payment payment;
|
||||||
|
|
||||||
|
public PaymentReportedEvent(Object source, Order order, Payment payment) {
|
||||||
|
super(source);
|
||||||
|
this.order = order;
|
||||||
|
this.payment = payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Order getOrder() {
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Payment getPayment() {
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package com.printcalculator.event.listener;
|
package com.printcalculator.event.listener;
|
||||||
|
|
||||||
import com.printcalculator.entity.Order;
|
import com.printcalculator.entity.Order;
|
||||||
|
import com.printcalculator.entity.Payment;
|
||||||
import com.printcalculator.event.OrderCreatedEvent;
|
import com.printcalculator.event.OrderCreatedEvent;
|
||||||
|
import com.printcalculator.event.PaymentReportedEvent;
|
||||||
import com.printcalculator.service.email.EmailNotificationService;
|
import com.printcalculator.service.email.EmailNotificationService;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
@@ -47,6 +49,19 @@ public class OrderEmailListener {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Async
|
||||||
|
@EventListener
|
||||||
|
public void handlePaymentReportedEvent(PaymentReportedEvent event) {
|
||||||
|
Order order = event.getOrder();
|
||||||
|
log.info("Processing PaymentReportedEvent for order id: {}", order.getId());
|
||||||
|
|
||||||
|
try {
|
||||||
|
sendPaymentReportedEmail(order);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to send payment reported email for order id: {}", order.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void sendCustomerConfirmationEmail(Order order) {
|
private void sendCustomerConfirmationEmail(Order order) {
|
||||||
Map<String, Object> templateData = new HashMap<>();
|
Map<String, Object> templateData = new HashMap<>();
|
||||||
templateData.put("customerName", order.getCustomer().getFirstName());
|
templateData.put("customerName", order.getCustomer().getFirstName());
|
||||||
@@ -64,6 +79,21 @@ public class OrderEmailListener {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void sendPaymentReportedEmail(Order order) {
|
||||||
|
Map<String, Object> templateData = new HashMap<>();
|
||||||
|
templateData.put("customerName", order.getCustomer().getFirstName());
|
||||||
|
templateData.put("orderId", order.getId());
|
||||||
|
templateData.put("orderNumber", getDisplayOrderNumber(order));
|
||||||
|
templateData.put("orderDetailsUrl", buildOrderDetailsUrl(order));
|
||||||
|
|
||||||
|
emailNotificationService.sendEmail(
|
||||||
|
order.getCustomer().getEmail(),
|
||||||
|
"Stiamo verificando il tuo pagamento (Ordine #" + getDisplayOrderNumber(order) + ")",
|
||||||
|
"payment-reported",
|
||||||
|
templateData
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private void sendAdminNotificationEmail(Order order) {
|
private void sendAdminNotificationEmail(Order order) {
|
||||||
Map<String, Object> templateData = new HashMap<>();
|
Map<String, Object> templateData = new HashMap<>();
|
||||||
templateData.put("customerName", order.getCustomer().getFirstName() + " " + order.getCustomer().getLastName());
|
templateData.put("customerName", order.getCustomer().getFirstName() + " " + order.getCustomer().getLastName());
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package com.printcalculator.repository;
|
|||||||
import com.printcalculator.entity.Payment;
|
import com.printcalculator.entity.Payment;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface PaymentRepository extends JpaRepository<Payment, UUID> {
|
public interface PaymentRepository extends JpaRepository<Payment, UUID> {
|
||||||
|
Optional<Payment> findByOrder_Id(UUID orderId);
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,7 @@ public class OrderService {
|
|||||||
private final InvoicePdfRenderingService invoiceService;
|
private final InvoicePdfRenderingService invoiceService;
|
||||||
private final QrBillService qrBillService;
|
private final QrBillService qrBillService;
|
||||||
private final ApplicationEventPublisher eventPublisher;
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
private final PaymentService paymentService;
|
||||||
|
|
||||||
public OrderService(OrderRepository orderRepo,
|
public OrderService(OrderRepository orderRepo,
|
||||||
OrderItemRepository orderItemRepo,
|
OrderItemRepository orderItemRepo,
|
||||||
@@ -45,7 +46,8 @@ public class OrderService {
|
|||||||
StorageService storageService,
|
StorageService storageService,
|
||||||
InvoicePdfRenderingService invoiceService,
|
InvoicePdfRenderingService invoiceService,
|
||||||
QrBillService qrBillService,
|
QrBillService qrBillService,
|
||||||
ApplicationEventPublisher eventPublisher) {
|
ApplicationEventPublisher eventPublisher,
|
||||||
|
PaymentService paymentService) {
|
||||||
this.orderRepo = orderRepo;
|
this.orderRepo = orderRepo;
|
||||||
this.orderItemRepo = orderItemRepo;
|
this.orderItemRepo = orderItemRepo;
|
||||||
this.quoteSessionRepo = quoteSessionRepo;
|
this.quoteSessionRepo = quoteSessionRepo;
|
||||||
@@ -55,6 +57,7 @@ public class OrderService {
|
|||||||
this.invoiceService = invoiceService;
|
this.invoiceService = invoiceService;
|
||||||
this.qrBillService = qrBillService;
|
this.qrBillService = qrBillService;
|
||||||
this.eventPublisher = eventPublisher;
|
this.eventPublisher = eventPublisher;
|
||||||
|
this.paymentService = paymentService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -201,6 +204,9 @@ public class OrderService {
|
|||||||
|
|
||||||
Order savedOrder = orderRepo.save(order);
|
Order savedOrder = orderRepo.save(order);
|
||||||
|
|
||||||
|
// ALWAYS initialize payment as PENDING
|
||||||
|
paymentService.getOrCreatePaymentForOrder(savedOrder, "OTHER");
|
||||||
|
|
||||||
eventPublisher.publishEvent(new OrderCreatedEvent(this, savedOrder));
|
eventPublisher.publishEvent(new OrderCreatedEvent(this, savedOrder));
|
||||||
|
|
||||||
return savedOrder;
|
return savedOrder;
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.printcalculator.service;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.Order;
|
||||||
|
import com.printcalculator.entity.Payment;
|
||||||
|
import com.printcalculator.event.PaymentReportedEvent;
|
||||||
|
import com.printcalculator.repository.OrderRepository;
|
||||||
|
import com.printcalculator.repository.PaymentRepository;
|
||||||
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class PaymentService {
|
||||||
|
|
||||||
|
private final PaymentRepository paymentRepo;
|
||||||
|
private final OrderRepository orderRepo;
|
||||||
|
private final ApplicationEventPublisher eventPublisher;
|
||||||
|
|
||||||
|
public PaymentService(PaymentRepository paymentRepo,
|
||||||
|
OrderRepository orderRepo,
|
||||||
|
ApplicationEventPublisher eventPublisher) {
|
||||||
|
this.paymentRepo = paymentRepo;
|
||||||
|
this.orderRepo = orderRepo;
|
||||||
|
this.eventPublisher = eventPublisher;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Payment getOrCreatePaymentForOrder(Order order, String defaultMethod) {
|
||||||
|
Optional<Payment> existing = paymentRepo.findByOrder_Id(order.getId());
|
||||||
|
if (existing.isPresent()) {
|
||||||
|
return existing.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Payment payment = new Payment();
|
||||||
|
payment.setOrder(order);
|
||||||
|
payment.setMethod(defaultMethod != null ? defaultMethod : "OTHER");
|
||||||
|
payment.setStatus("PENDING");
|
||||||
|
payment.setCurrency(order.getCurrency() != null ? order.getCurrency() : "CHF");
|
||||||
|
payment.setAmountChf(order.getTotalChf() != null ? order.getTotalChf() : BigDecimal.ZERO);
|
||||||
|
payment.setInitiatedAt(OffsetDateTime.now());
|
||||||
|
|
||||||
|
return paymentRepo.save(payment);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public Payment reportPayment(UUID orderId, String method) {
|
||||||
|
Order order = orderRepo.findById(orderId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("Order not found with id " + orderId));
|
||||||
|
|
||||||
|
Payment payment = paymentRepo.findByOrder_Id(orderId)
|
||||||
|
.orElseThrow(() -> new RuntimeException("No active payment found for order " + orderId));
|
||||||
|
|
||||||
|
if (!"PENDING".equals(payment.getStatus())) {
|
||||||
|
throw new IllegalStateException("Payment is not in PENDING state. Current state: " + payment.getStatus());
|
||||||
|
}
|
||||||
|
|
||||||
|
payment.setStatus("REPORTED");
|
||||||
|
payment.setReportedAt(OffsetDateTime.now());
|
||||||
|
if (method != null && !method.isBlank()) {
|
||||||
|
payment.setMethod(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
payment = paymentRepo.save(payment);
|
||||||
|
|
||||||
|
eventPublisher.publishEvent(new PaymentReportedEvent(this, order, payment));
|
||||||
|
|
||||||
|
return payment;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import java.io.IOException;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
@@ -17,12 +18,15 @@ import java.util.stream.Stream;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ProfileManager {
|
public class ProfileManager {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(ProfileManager.class.getName());
|
private static final Logger logger = Logger.getLogger(ProfileManager.class.getName());
|
||||||
private final String profilesRoot;
|
private final String profilesRoot;
|
||||||
|
private final Path resolvedProfilesRoot;
|
||||||
private final ObjectMapper mapper;
|
private final ObjectMapper mapper;
|
||||||
|
|
||||||
private final Map<String, String> profileAliases;
|
private final Map<String, String> profileAliases;
|
||||||
@@ -32,6 +36,8 @@ public class ProfileManager {
|
|||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
this.profileAliases = new HashMap<>();
|
this.profileAliases = new HashMap<>();
|
||||||
initializeAliases();
|
initializeAliases();
|
||||||
|
this.resolvedProfilesRoot = resolveProfilesRoot(profilesRoot);
|
||||||
|
logger.info("Profiles root configured as '" + this.profilesRoot + "', resolved to '" + this.resolvedProfilesRoot + "'");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeAliases() {
|
private void initializeAliases() {
|
||||||
@@ -55,13 +61,18 @@ public class ProfileManager {
|
|||||||
public ObjectNode getMergedProfile(String profileName, String type) throws IOException {
|
public ObjectNode getMergedProfile(String profileName, String type) throws IOException {
|
||||||
Path profilePath = findProfileFile(profileName, type);
|
Path profilePath = findProfileFile(profileName, type);
|
||||||
if (profilePath == null) {
|
if (profilePath == null) {
|
||||||
throw new IOException("Profile not found: " + profileName);
|
throw new IOException("Profile not found: " + profileName + " (root=" + resolvedProfilesRoot + ")");
|
||||||
}
|
}
|
||||||
logger.info("Resolved " + type + " profile '" + profileName + "' -> " + profilePath);
|
logger.info("Resolved " + type + " profile '" + profileName + "' -> " + profilePath);
|
||||||
return resolveInheritance(profilePath);
|
return resolveInheritance(profilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Path findProfileFile(String name, String type) {
|
private Path findProfileFile(String name, String type) {
|
||||||
|
if (!Files.isDirectory(resolvedProfilesRoot)) {
|
||||||
|
logger.severe("Profiles root does not exist or is not a directory: " + resolvedProfilesRoot);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Check aliases first
|
// Check aliases first
|
||||||
String resolvedName = profileAliases.getOrDefault(name, name);
|
String resolvedName = profileAliases.getOrDefault(name, name);
|
||||||
|
|
||||||
@@ -69,7 +80,7 @@ public class ProfileManager {
|
|||||||
// collisions across vendors/profile families with same filename.
|
// collisions across vendors/profile families with same filename.
|
||||||
String filename = toJsonFilename(resolvedName);
|
String filename = toJsonFilename(resolvedName);
|
||||||
|
|
||||||
try (Stream<Path> stream = Files.walk(Paths.get(profilesRoot))) {
|
try (Stream<Path> stream = Files.walk(resolvedProfilesRoot)) {
|
||||||
List<Path> candidates = stream
|
List<Path> candidates = stream
|
||||||
.filter(p -> p.getFileName().toString().equals(filename))
|
.filter(p -> p.getFileName().toString().equals(filename))
|
||||||
.sorted()
|
.sorted()
|
||||||
@@ -95,6 +106,37 @@ public class ProfileManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Path resolveProfilesRoot(String configuredRoot) {
|
||||||
|
Set<Path> candidates = new LinkedHashSet<>();
|
||||||
|
Path cwd = Paths.get("").toAbsolutePath().normalize();
|
||||||
|
|
||||||
|
if (configuredRoot != null && !configuredRoot.isBlank()) {
|
||||||
|
Path configured = Paths.get(configuredRoot);
|
||||||
|
candidates.add(configured.toAbsolutePath().normalize());
|
||||||
|
if (!configured.isAbsolute()) {
|
||||||
|
candidates.add(cwd.resolve(configuredRoot).normalize());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.add(cwd.resolve("profiles").normalize());
|
||||||
|
candidates.add(cwd.resolve("backend/profiles").normalize());
|
||||||
|
candidates.add(Paths.get("/app/profiles").toAbsolutePath().normalize());
|
||||||
|
|
||||||
|
List<String> checkedPaths = new ArrayList<>();
|
||||||
|
for (Path candidate : candidates) {
|
||||||
|
checkedPaths.add(candidate.toString());
|
||||||
|
if (Files.isDirectory(candidate)) {
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.warning("No profiles directory found. Checked: " + String.join(", ", checkedPaths));
|
||||||
|
if (configuredRoot != null && !configuredRoot.isBlank()) {
|
||||||
|
return Paths.get(configuredRoot).toAbsolutePath().normalize();
|
||||||
|
}
|
||||||
|
return cwd.resolve("profiles").normalize();
|
||||||
|
}
|
||||||
|
|
||||||
private ObjectNode resolveInheritance(Path currentPath) throws IOException {
|
private ObjectNode resolveInheritance(Path currentPath) throws IOException {
|
||||||
// 1. Load current
|
// 1. Load current
|
||||||
JsonNode currentNode = mapper.readTree(currentPath.toFile());
|
JsonNode currentNode = mapper.readTree(currentPath.toFile());
|
||||||
|
|||||||
@@ -24,8 +24,16 @@ public class SmtpEmailNotificationService implements EmailNotificationService {
|
|||||||
@Value("${app.mail.from}")
|
@Value("${app.mail.from}")
|
||||||
private String fromAddress;
|
private String fromAddress;
|
||||||
|
|
||||||
|
@Value("${app.mail.enabled:true}")
|
||||||
|
private boolean mailEnabled;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void sendEmail(String to, String subject, String templateName, Map<String, Object> contextData) {
|
public void sendEmail(String to, String subject, String templateName, Map<String, Object> contextData) {
|
||||||
|
if (!mailEnabled) {
|
||||||
|
log.info("Email sending disabled (app.mail.enabled=false). Skipping email to {}", to);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
log.info("Preparing to send email to {} with template {}", to, templateName);
|
log.info("Preparing to send email to {} with template {}", to, templateName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
2
backend/src/main/resources/application-local.properties
Normal file
2
backend/src/main/resources/application-local.properties
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
app.mail.enabled=false
|
||||||
|
app.mail.admin.enabled=false
|
||||||
@@ -36,6 +36,7 @@ spring.mail.properties.mail.smtp.auth=${MAIL_SMTP_AUTH:false}
|
|||||||
spring.mail.properties.mail.smtp.starttls.enable=${MAIL_SMTP_STARTTLS:false}
|
spring.mail.properties.mail.smtp.starttls.enable=${MAIL_SMTP_STARTTLS:false}
|
||||||
|
|
||||||
# Application Mail Settings
|
# Application Mail Settings
|
||||||
|
app.mail.enabled=${APP_MAIL_ENABLED:true}
|
||||||
app.mail.from=${APP_MAIL_FROM:${MAIL_USERNAME:noreply@printcalculator.local}}
|
app.mail.from=${APP_MAIL_FROM:${MAIL_USERNAME:noreply@printcalculator.local}}
|
||||||
app.mail.admin.enabled=${APP_MAIL_ADMIN_ENABLED:true}
|
app.mail.admin.enabled=${APP_MAIL_ADMIN_ENABLED:true}
|
||||||
app.mail.admin.address=${APP_MAIL_ADMIN_ADDRESS:admin@printcalculator.local}
|
app.mail.admin.address=${APP_MAIL_ADMIN_ADDRESS:admin@printcalculator.local}
|
||||||
|
|||||||
@@ -3,17 +3,18 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<style>
|
<style>
|
||||||
@page invoice { size: 8.5in 11in; margin: 0.65in; }
|
@page invoice { size: A4; margin: 14mm 14mm 12mm 14mm; }
|
||||||
@page qrpage { size: A4; margin: 0; }
|
@page qrpage { size: A4; margin: 0; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
page: invoice;
|
page: invoice;
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||||
font-size: 10pt;
|
font-size: 9.5pt;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #1a1a1a;
|
color: #191919;
|
||||||
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
.invoice-page {
|
.invoice-page {
|
||||||
@@ -21,112 +22,186 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-table {
|
.top-layout {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
table-layout: fixed;
|
margin-bottom: 8mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-table td {
|
.top-layout td {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-left {
|
.doc-title {
|
||||||
width: 58%;
|
font-size: 18pt;
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-right {
|
|
||||||
width: 42%;
|
|
||||||
text-align: right;
|
|
||||||
line-height: 1.35;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invoice-title {
|
|
||||||
font-size: 15pt;
|
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin: 0 0 4mm 0;
|
margin: 0 0 1.5mm 0;
|
||||||
letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-title {
|
|
||||||
margin: 9mm 0 2mm 0;
|
|
||||||
font-size: 10.5pt;
|
|
||||||
font-weight: 700;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.2px;
|
letter-spacing: 0.2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buyer-box {
|
.doc-subtitle {
|
||||||
margin: 0;
|
color: #4b4b4b;
|
||||||
line-height: 1.4;
|
font-size: 10pt;
|
||||||
min-height: 20mm;
|
}
|
||||||
|
|
||||||
|
.seller-block {
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.45;
|
||||||
|
width: 42%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seller-name {
|
||||||
|
font-size: 11pt;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-layout {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 8mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-layout td {
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.order-details {
|
||||||
|
width: 60%;
|
||||||
|
padding-right: 5mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customer-box {
|
||||||
|
width: 40%;
|
||||||
|
background: #f7f7f7;
|
||||||
|
border: 1px solid #e2e2e2;
|
||||||
|
padding: 3mm 3.2mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-title {
|
||||||
|
font-size: 8.8pt;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
color: #5a5a5a;
|
||||||
|
margin-bottom: 2mm;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-table td {
|
||||||
|
padding: 1.1mm 0;
|
||||||
|
border-bottom: 1px solid #ececec;
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-label {
|
||||||
|
color: #636363;
|
||||||
|
width: 56%;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-right: 3mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-value {
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-items {
|
.line-items {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
margin-top: 8mm;
|
margin-top: 3mm;
|
||||||
|
border-top: 1px solid #cfcfcf;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-items th,
|
.line-items th,
|
||||||
.line-items td {
|
.line-items td {
|
||||||
border-bottom: 1px solid #d8d8d8;
|
border-bottom: 1px solid #dedede;
|
||||||
padding: 2.8mm 2.2mm;
|
padding: 2.4mm 2mm;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
word-break: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-items th {
|
.line-items th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: #f7f7f7;
|
background: #f2f2f2;
|
||||||
|
color: #2c2c2c;
|
||||||
|
font-size: 9pt;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-items th:nth-child(1),
|
.line-items th:nth-child(1),
|
||||||
.line-items td:nth-child(1) {
|
.line-items td:nth-child(1) {
|
||||||
width: 54%;
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-items th:nth-child(2),
|
.line-items th:nth-child(2),
|
||||||
.line-items td:nth-child(2) {
|
.line-items td:nth-child(2) {
|
||||||
width: 12%;
|
width: 10%;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-items th:nth-child(3),
|
.line-items th:nth-child(3),
|
||||||
.line-items td:nth-child(3) {
|
.line-items td:nth-child(3) {
|
||||||
width: 17%;
|
width: 20%;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-items th:nth-child(4),
|
.line-items th:nth-child(4),
|
||||||
.line-items td:nth-child(4) {
|
.line-items td:nth-child(4) {
|
||||||
width: 17%;
|
width: 20%;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.summary-layout {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 6mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-layout td {
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes {
|
||||||
|
width: 58%;
|
||||||
|
padding-right: 5mm;
|
||||||
|
color: #383838;
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notes .section-caption {
|
||||||
|
font-weight: 700;
|
||||||
|
margin: 0 0 1.2mm 0;
|
||||||
|
color: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
.totals {
|
.totals {
|
||||||
margin-top: 7mm;
|
width: 42%;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
width: 76mm;
|
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totals td {
|
.totals td {
|
||||||
border: none;
|
border: none;
|
||||||
padding: 1.6mm 0;
|
padding: 1.3mm 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totals-label {
|
.totals-label {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: #3a3a3a;
|
color: #4a4a4a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.totals-value {
|
.totals-value {
|
||||||
@@ -136,16 +211,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.total-strong td {
|
.total-strong td {
|
||||||
font-size: 11pt;
|
font-size: 10.5pt;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding-top: 2.4mm;
|
padding-top: 2mm;
|
||||||
border-top: 1px solid #d8d8d8;
|
border-top: 1px solid #cfcfcf;
|
||||||
}
|
}
|
||||||
|
|
||||||
.payment-terms {
|
.due-row td {
|
||||||
margin-top: 9mm;
|
font-size: 10pt;
|
||||||
line-height: 1.4;
|
font-weight: 700;
|
||||||
color: #2b2b2b;
|
border-top: 1px solid #cfcfcf;
|
||||||
|
padding-top: 2.2mm;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-only-page {
|
.qr-only-page {
|
||||||
@@ -155,7 +231,6 @@
|
|||||||
height: 297mm;
|
height: 297mm;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
page-break-before: always;
|
page-break-before: always;
|
||||||
break-before: page;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-bill-bottom {
|
.qr-bill-bottom {
|
||||||
@@ -178,38 +253,58 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="invoice-page">
|
<div class="invoice-page">
|
||||||
|
|
||||||
<table class="header-table">
|
<table class="top-layout">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="header-left">
|
<td>
|
||||||
<div><strong th:text="${sellerDisplayName}">Nome Cognome</strong></div>
|
<div class="doc-title">Conferma ordine</div>
|
||||||
<div th:text="${sellerAddressLine1}">Via Esempio 12</div>
|
<div class="doc-subtitle">Ricevuta semplificata</div>
|
||||||
<div th:text="${sellerAddressLine2}">6500 Bellinzona, CH</div>
|
|
||||||
<div th:text="${sellerEmail}">email@example.com</div>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="header-right">
|
<td class="seller-block">
|
||||||
<div class="invoice-title">Fattura</div>
|
<div class="seller-name" th:text="${sellerDisplayName}">3D Fab Switzerland</div>
|
||||||
<div>Numero: <span th:text="${invoiceNumber}">2026-000123</span></div>
|
<div th:text="${sellerAddressLine1}">Sede Ticino, Svizzera</div>
|
||||||
<div>Data: <span th:text="${invoiceDate}">2026-02-13</span></div>
|
<div th:text="${sellerAddressLine2}">Sede Bienne, Svizzera</div>
|
||||||
<div>Scadenza: <span th:text="${dueDate}">2026-02-20</span></div>
|
<div th:text="${sellerEmail}">info@3dfab.ch</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="section-title">Fatturare a</div>
|
<table class="meta-layout">
|
||||||
<div class="buyer-box">
|
<tr>
|
||||||
<div>
|
<td class="order-details">
|
||||||
<div th:text="${buyerDisplayName}">Cliente SA</div>
|
<table class="details-table">
|
||||||
<div th:text="${buyerAddressLine1}">Via Cliente 7</div>
|
<tr>
|
||||||
<div th:text="${buyerAddressLine2}">8000 Zürich, CH</div>
|
<td class="details-label">Data ordine / fattura</td>
|
||||||
</div>
|
<td class="details-value" th:text="${invoiceDate}">2026-02-13</td>
|
||||||
</div>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="details-label">Numero documento</td>
|
||||||
|
<td class="details-value" th:text="${invoiceNumber}">INV-2026-000123</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="details-label">Data di scadenza</td>
|
||||||
|
<td class="details-value" th:text="${dueDate}">2026-02-20</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="details-label">Valuta</td>
|
||||||
|
<td class="details-value">CHF</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td class="customer-box">
|
||||||
|
<div class="box-title">Cliente</div>
|
||||||
|
<div th:text="${buyerDisplayName}">Cliente SA</div>
|
||||||
|
<div th:text="${buyerAddressLine1}">Via Cliente 7</div>
|
||||||
|
<div th:text="${buyerAddressLine2}">8000 Zürich, CH</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<table class="line-items">
|
<table class="line-items">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Descrizione</th>
|
<th>Descrizione</th>
|
||||||
<th>Qtà</th>
|
<th>Qtà</th>
|
||||||
<th>Prezzo</th>
|
<th>Prezzo unit.</th>
|
||||||
<th>Totale</th>
|
<th>Totale</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -223,21 +318,36 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<table class="totals">
|
<table class="summary-layout">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="totals-label">Subtotale</td>
|
<td class="notes">
|
||||||
<td class="totals-value" th:text="${subtotalFormatted}">CHF 10.00</td>
|
<div class="section-caption">Informazioni</div>
|
||||||
</tr>
|
<div th:text="${paymentTermsText}">
|
||||||
<tr class="total-strong">
|
Appena riceviamo il pagamento l'ordine entra nella coda di stampa. Grazie per la fiducia.
|
||||||
<td class="totals-label">Totale</td>
|
</div>
|
||||||
<td class="totals-value" th:text="${grandTotalFormatted}">CHF 10.00</td>
|
<div style="margin-top: 2.5mm;">
|
||||||
|
Verifica i dettagli dell'ordine al ricevimento. Per assistenza, rispondi alla nostra email di conferma.
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<table class="totals">
|
||||||
|
<tr>
|
||||||
|
<td class="totals-label">Subtotale</td>
|
||||||
|
<td class="totals-value" th:text="${subtotalFormatted}">CHF 10.00</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="total-strong">
|
||||||
|
<td class="totals-label">Totale ordine</td>
|
||||||
|
<td class="totals-value" th:text="${grandTotalFormatted}">CHF 10.00</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="due-row">
|
||||||
|
<td class="totals-label">Importo dovuto</td>
|
||||||
|
<td class="totals-value" th:text="${grandTotalFormatted}">CHF 10.00</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="payment-terms" th:text="${paymentTermsText}">
|
|
||||||
Pagamento entro 7 giorni. Grazie.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="qr-only-page">
|
<div class="qr-only-page">
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.printcalculator.event.listener;
|
||||||
|
|
||||||
|
import com.printcalculator.entity.Customer;
|
||||||
|
import com.printcalculator.entity.Order;
|
||||||
|
import com.printcalculator.event.OrderCreatedEvent;
|
||||||
|
import com.printcalculator.service.email.EmailNotificationService;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.Captor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class OrderEmailListenerTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private EmailNotificationService emailNotificationService;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private OrderEmailListener orderEmailListener;
|
||||||
|
|
||||||
|
@Captor
|
||||||
|
private ArgumentCaptor<Map<String, Object>> templateDataCaptor;
|
||||||
|
|
||||||
|
private Order order;
|
||||||
|
private OrderCreatedEvent event;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
Customer customer = new Customer();
|
||||||
|
customer.setFirstName("John");
|
||||||
|
customer.setLastName("Doe");
|
||||||
|
customer.setEmail("john.doe@test.com");
|
||||||
|
|
||||||
|
order = new Order();
|
||||||
|
order.setId(UUID.randomUUID());
|
||||||
|
order.setCustomer(customer);
|
||||||
|
order.setCreatedAt(OffsetDateTime.parse("2026-02-21T10:00:00Z"));
|
||||||
|
order.setTotalChf(new BigDecimal("150.50"));
|
||||||
|
|
||||||
|
event = new OrderCreatedEvent(this, order);
|
||||||
|
|
||||||
|
ReflectionTestUtils.setField(orderEmailListener, "adminMailEnabled", true);
|
||||||
|
ReflectionTestUtils.setField(orderEmailListener, "adminMailAddress", "admin@printcalculator.local");
|
||||||
|
ReflectionTestUtils.setField(orderEmailListener, "frontendBaseUrl", "https://tuosito.it");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handleOrderCreatedEvent_ShouldSendCustomerAndAdminEmails() {
|
||||||
|
// Act
|
||||||
|
orderEmailListener.handleOrderCreatedEvent(event);
|
||||||
|
|
||||||
|
// Assert Customer Email
|
||||||
|
verify(emailNotificationService, times(1)).sendEmail(
|
||||||
|
eq("john.doe@test.com"),
|
||||||
|
eq("Conferma Ordine #" + order.getOrderNumber() + " - 3D-Fab"),
|
||||||
|
eq("order-confirmation"),
|
||||||
|
templateDataCaptor.capture()
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object> customerData = templateDataCaptor.getAllValues().get(0);
|
||||||
|
assertEquals("John", customerData.get("customerName"));
|
||||||
|
assertEquals(order.getId(), customerData.get("orderId"));
|
||||||
|
assertEquals(order.getOrderNumber(), customerData.get("orderNumber"));
|
||||||
|
assertEquals("https://tuosito.it/ordine/" + order.getId(), customerData.get("orderDetailsUrl"));
|
||||||
|
assertEquals(order.getCreatedAt().format(DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm")), customerData.get("orderDate"));
|
||||||
|
assertEquals("150.50", customerData.get("totalCost"));
|
||||||
|
|
||||||
|
// Assert Admin Email
|
||||||
|
verify(emailNotificationService, times(1)).sendEmail(
|
||||||
|
eq("admin@printcalculator.local"),
|
||||||
|
eq("Nuovo Ordine Ricevuto #" + order.getOrderNumber() + " - Doe"),
|
||||||
|
eq("order-confirmation"),
|
||||||
|
templateDataCaptor.capture()
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object> adminData = templateDataCaptor.getAllValues().get(1);
|
||||||
|
assertEquals("John Doe", adminData.get("customerName"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handleOrderCreatedEvent_WithAdminDisabled_ShouldOnlySendCustomerEmail() {
|
||||||
|
// Arrange
|
||||||
|
ReflectionTestUtils.setField(orderEmailListener, "adminMailEnabled", false);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
orderEmailListener.handleOrderCreatedEvent(event);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(emailNotificationService, times(1)).sendEmail(
|
||||||
|
eq("john.doe@test.com"),
|
||||||
|
anyString(),
|
||||||
|
anyString(),
|
||||||
|
any()
|
||||||
|
);
|
||||||
|
|
||||||
|
verify(emailNotificationService, never()).sendEmail(
|
||||||
|
eq("admin@printcalculator.local"),
|
||||||
|
anyString(),
|
||||||
|
anyString(),
|
||||||
|
any()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void handleOrderCreatedEvent_ExceptionHandling_ShouldNotPropagate() {
|
||||||
|
// Arrange
|
||||||
|
doThrow(new RuntimeException("Simulated Mail Failure"))
|
||||||
|
.when(emailNotificationService).sendEmail(anyString(), anyString(), anyString(), any());
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
// Event listener shouldn't throw exception back, thus passing the test.
|
||||||
|
orderEmailListener.handleOrderCreatedEvent(event);
|
||||||
|
|
||||||
|
verify(emailNotificationService, times(1)).sendEmail(anyString(), anyString(), anyString(), any());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.printcalculator.service.email;
|
||||||
|
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.ArgumentCaptor;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
|
import org.thymeleaf.TemplateEngine;
|
||||||
|
import org.thymeleaf.context.Context;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class SmtpEmailNotificationServiceTest {
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private JavaMailSender emailSender;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private TemplateEngine templateEngine;
|
||||||
|
|
||||||
|
@Mock
|
||||||
|
private MimeMessage mimeMessage;
|
||||||
|
|
||||||
|
@InjectMocks
|
||||||
|
private SmtpEmailNotificationService emailNotificationService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
ReflectionTestUtils.setField(emailNotificationService, "fromAddress", "noreply@test.com");
|
||||||
|
ReflectionTestUtils.setField(emailNotificationService, "mailEnabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendEmail_Success() {
|
||||||
|
// Arrange
|
||||||
|
String to = "user@test.com";
|
||||||
|
String subject = "Test Subject";
|
||||||
|
String templateName = "test-template";
|
||||||
|
Map<String, Object> contextData = new HashMap<>();
|
||||||
|
contextData.put("key", "value");
|
||||||
|
|
||||||
|
when(templateEngine.process(eq("email/" + templateName), any(Context.class))).thenReturn("<html>Test</html>");
|
||||||
|
when(emailSender.createMimeMessage()).thenReturn(mimeMessage);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
emailNotificationService.sendEmail(to, subject, templateName, contextData);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
verify(templateEngine, times(1)).process(eq("email/" + templateName), any(Context.class));
|
||||||
|
verify(emailSender, times(1)).createMimeMessage();
|
||||||
|
verify(emailSender, times(1)).send(mimeMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void sendEmail_Exception_ShouldNotThrow() {
|
||||||
|
// Arrange
|
||||||
|
String to = "user@test.com";
|
||||||
|
String subject = "Test Subject";
|
||||||
|
String templateName = "test-template";
|
||||||
|
Map<String, Object> contextData = new HashMap<>();
|
||||||
|
|
||||||
|
when(templateEngine.process(eq("email/" + templateName), any(Context.class))).thenThrow(new RuntimeException("Template error"));
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
// We expect the exception to be caught and logged, not propagated
|
||||||
|
assertDoesNotThrow(() -> emailNotificationService.sendEmail(to, subject, templateName, contextData));
|
||||||
|
|
||||||
|
verify(emailSender, never()).createMimeMessage();
|
||||||
|
verify(emailSender, never()).send(any(MimeMessage.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
3
db.sql
3
db.sql
@@ -553,7 +553,7 @@ CREATE TABLE IF NOT EXISTS payments
|
|||||||
order_id uuid NOT NULL REFERENCES orders (order_id) ON DELETE CASCADE,
|
order_id uuid NOT NULL REFERENCES orders (order_id) ON DELETE CASCADE,
|
||||||
|
|
||||||
method text NOT NULL CHECK (method IN ('QR_BILL', 'BANK_TRANSFER', 'TWINT', 'CARD', 'OTHER')),
|
method text NOT NULL CHECK (method IN ('QR_BILL', 'BANK_TRANSFER', 'TWINT', 'CARD', 'OTHER')),
|
||||||
status text NOT NULL CHECK (status IN ('PENDING', 'RECEIVED', 'FAILED', 'CANCELLED', 'REFUNDED')),
|
status text NOT NULL CHECK (status IN ('PENDING', 'REPORTED', 'RECEIVED', 'FAILED', 'CANCELLED', 'REFUNDED')),
|
||||||
|
|
||||||
currency char(3) NOT NULL DEFAULT 'CHF',
|
currency char(3) NOT NULL DEFAULT 'CHF',
|
||||||
amount_chf numeric(12, 2) NOT NULL CHECK (amount_chf >= 0),
|
amount_chf numeric(12, 2) NOT NULL CHECK (amount_chf >= 0),
|
||||||
@@ -564,6 +564,7 @@ CREATE TABLE IF NOT EXISTS payments
|
|||||||
|
|
||||||
qr_payload text, -- se vuoi salvare contenuto QR/Swiss QR bill
|
qr_payload text, -- se vuoi salvare contenuto QR/Swiss QR bill
|
||||||
initiated_at timestamptz NOT NULL DEFAULT now(),
|
initiated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
reported_at timestamptz,
|
||||||
received_at timestamptz
|
received_at timestamptz
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,44 +1,4 @@
|
|||||||
services:
|
services:
|
||||||
backend:
|
|
||||||
platform: linux/amd64
|
|
||||||
build:
|
|
||||||
context: ./backend
|
|
||||||
platforms:
|
|
||||||
- linux/amd64
|
|
||||||
container_name: print-calculator-backend
|
|
||||||
ports:
|
|
||||||
- "8000:8000"
|
|
||||||
environment:
|
|
||||||
- DB_URL=jdbc:postgresql://db:5432/printcalc
|
|
||||||
- DB_USERNAME=printcalc
|
|
||||||
- DB_PASSWORD=printcalc_secret
|
|
||||||
- SPRING_PROFILES_ACTIVE=local
|
|
||||||
- FILAMENT_COST_PER_KG=22.0
|
|
||||||
- MACHINE_COST_PER_HOUR=2.50
|
|
||||||
- ENERGY_COST_PER_KWH=0.30
|
|
||||||
- PRINTER_POWER_WATTS=150
|
|
||||||
- MARKUP_PERCENT=20
|
|
||||||
- TEMP_DIR=/app/temp
|
|
||||||
- PROFILES_DIR=/app/profiles
|
|
||||||
- CLAMAV_HOST=clamav
|
|
||||||
- CLAMAV_PORT=3310
|
|
||||||
depends_on:
|
|
||||||
- db
|
|
||||||
- clamav
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
frontend:
|
|
||||||
build:
|
|
||||||
context: ./frontend
|
|
||||||
dockerfile: Dockerfile.dev
|
|
||||||
container_name: print-calculator-frontend
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
depends_on:
|
|
||||||
- backend
|
|
||||||
- db
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
db:
|
db:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
container_name: print-calculator-db
|
container_name: print-calculator-db
|
||||||
|
|||||||
@@ -152,6 +152,13 @@ export class QuoteEstimatorService {
|
|||||||
return this.http.get(`${environment.apiUrl}/api/orders/${orderId}`, { headers });
|
return this.http.get(`${environment.apiUrl}/api/orders/${orderId}`, { headers });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reportPayment(orderId: string, method: string): Observable<any> {
|
||||||
|
const headers: any = {};
|
||||||
|
// @ts-ignore
|
||||||
|
if (environment.basicAuth) headers['Authorization'] = 'Basic ' + btoa(environment.basicAuth);
|
||||||
|
return this.http.post(`${environment.apiUrl}/api/orders/${orderId}/payments/report`, { method }, { headers });
|
||||||
|
}
|
||||||
|
|
||||||
getOrderInvoice(orderId: string): Observable<Blob> {
|
getOrderInvoice(orderId: string): Observable<Blob> {
|
||||||
const headers: any = {};
|
const headers: any = {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|||||||
@@ -4,14 +4,41 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="confirmation-layout">
|
<div class="confirmation-layout" *ngIf="order() as o">
|
||||||
<app-card class="status-card">
|
<app-card class="status-card">
|
||||||
<div class="status-badge">{{ 'ORDER_CONFIRMED.STATUS' | translate }}</div>
|
<div class="status-badge">{{ o.status === 'SHIPPED' ? ('TRACKING.STEP_SHIPPED' | translate) : ('ORDER_CONFIRMED.STATUS' | translate) }}</div>
|
||||||
<h2>{{ 'ORDER_CONFIRMED.HEADING' | translate }}</h2>
|
<h2>{{ 'ORDER_CONFIRMED.HEADING' | translate }}</h2>
|
||||||
<p class="order-ref" *ngIf="orderNumber">
|
<p class="order-ref" *ngIf="orderNumber">
|
||||||
{{ 'ORDER_CONFIRMED.ORDER_REF' | translate }}: <strong>#{{ orderNumber }}</strong>
|
{{ 'ORDER_CONFIRMED.ORDER_REF' | translate }}: <strong>#{{ orderNumber }}</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div class="status-timeline">
|
||||||
|
<div class="timeline-step"
|
||||||
|
[class.active]="o.status === 'PENDING_PAYMENT' && o.paymentStatus !== 'REPORTED'"
|
||||||
|
[class.completed]="o.paymentStatus === 'REPORTED' || o.status !== 'PENDING_PAYMENT'">
|
||||||
|
<div class="circle">1</div>
|
||||||
|
<div class="label">{{ 'TRACKING.STEP_PENDING' | translate }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-step"
|
||||||
|
[class.active]="o.paymentStatus === 'REPORTED' && o.status === 'PENDING_PAYMENT'"
|
||||||
|
[class.completed]="o.status === 'PAID' || o.status === 'IN_PRODUCTION' || o.status === 'SHIPPED' || o.status === 'COMPLETED'">
|
||||||
|
<div class="circle">2</div>
|
||||||
|
<div class="label">{{ 'TRACKING.STEP_REPORTED' | translate }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-step"
|
||||||
|
[class.active]="o.status === 'PAID' || o.status === 'IN_PRODUCTION'"
|
||||||
|
[class.completed]="o.status === 'SHIPPED' || o.status === 'COMPLETED'">
|
||||||
|
<div class="circle">3</div>
|
||||||
|
<div class="label">{{ 'TRACKING.STEP_PRODUCTION' | translate }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="timeline-step"
|
||||||
|
[class.active]="o.status === 'SHIPPED'"
|
||||||
|
[class.completed]="o.status === 'COMPLETED'">
|
||||||
|
<div class="circle">4</div>
|
||||||
|
<div class="label">{{ 'TRACKING.STEP_SHIPPED' | translate }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="message-block">
|
<div class="message-block">
|
||||||
<p>{{ 'ORDER_CONFIRMED.PROCESSING_TEXT' | translate }}</p>
|
<p>{{ 'ORDER_CONFIRMED.PROCESSING_TEXT' | translate }}</p>
|
||||||
<p>{{ 'ORDER_CONFIRMED.EMAIL_TEXT' | translate }}</p>
|
<p>{{ 'ORDER_CONFIRMED.EMAIL_TEXT' | translate }}</p>
|
||||||
|
|||||||
@@ -60,3 +60,100 @@ h2 {
|
|||||||
.actions {
|
.actions {
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-timeline {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--color-border);
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-step {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--color-neutral-100);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
.circle {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background: var(--color-primary-light);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
color: var(--color-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.completed {
|
||||||
|
.circle {
|
||||||
|
background: var(--color-primary);
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.label {
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.status-timeline {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-4);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
top: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
left: 15px;
|
||||||
|
width: 2px;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-step {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--space-3);
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, OnInit, inject, signal } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -20,6 +20,7 @@ export class OrderConfirmedComponent implements OnInit {
|
|||||||
|
|
||||||
orderId: string | null = null;
|
orderId: string | null = null;
|
||||||
orderNumber: string | null = null;
|
orderNumber: string | null = null;
|
||||||
|
order = signal<any>(null);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.orderId = this.route.snapshot.paramMap.get('orderId');
|
this.orderId = this.route.snapshot.paramMap.get('orderId');
|
||||||
@@ -30,6 +31,7 @@ export class OrderConfirmedComponent implements OnInit {
|
|||||||
this.orderNumber = this.extractOrderNumber(this.orderId);
|
this.orderNumber = this.extractOrderNumber(this.orderId);
|
||||||
this.quoteService.getOrder(this.orderId).subscribe({
|
this.quoteService.getOrder(this.orderId).subscribe({
|
||||||
next: (order) => {
|
next: (order) => {
|
||||||
|
this.order.set(order);
|
||||||
this.orderNumber = order?.orderNumber ?? this.orderNumber;
|
this.orderNumber = order?.orderNumber ?? this.orderNumber;
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
|
|||||||
@@ -6,6 +6,13 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="payment-layout" *ngIf="order() as o">
|
<div class="payment-layout" *ngIf="order() as o">
|
||||||
<div class="payment-main">
|
<div class="payment-main">
|
||||||
|
<app-card class="mb-6 status-reported-card" *ngIf="o.paymentStatus === 'REPORTED'">
|
||||||
|
<div class="status-content text-center">
|
||||||
|
<h3>{{ 'PAYMENT.STATUS_REPORTED_TITLE' | translate }}</h3>
|
||||||
|
<p>{{ 'PAYMENT.STATUS_REPORTED_DESC' | translate }}</p>
|
||||||
|
</div>
|
||||||
|
</app-card>
|
||||||
|
|
||||||
<app-card class="mb-6">
|
<app-card class="mb-6">
|
||||||
<div class="card-header-simple">
|
<div class="card-header-simple">
|
||||||
<h3>{{ 'PAYMENT.METHOD' | translate }}</h3>
|
<h3>{{ 'PAYMENT.METHOD' | translate }}</h3>
|
||||||
@@ -69,8 +76,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<app-button (click)="completeOrder()" [disabled]="!selectedPaymentMethod" [fullWidth]="true">
|
<app-button
|
||||||
{{ 'PAYMENT.CONFIRM' | translate }}
|
(click)="completeOrder()"
|
||||||
|
[disabled]="!selectedPaymentMethod || o.paymentStatus === 'REPORTED'"
|
||||||
|
[fullWidth]="true">
|
||||||
|
{{ o.paymentStatus === 'REPORTED' ? ('PAYMENT.IN_VERIFICATION' | translate) : ('PAYMENT.CONFIRM' | translate) }}
|
||||||
</app-button>
|
</app-button>
|
||||||
</div>
|
</div>
|
||||||
</app-card>
|
</app-card>
|
||||||
|
|||||||
@@ -116,11 +116,22 @@ export class PaymentComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
completeOrder(): void {
|
completeOrder(): void {
|
||||||
if (!this.orderId) {
|
if (!this.orderId || !this.selectedPaymentMethod) {
|
||||||
this.router.navigate(['/']);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.router.navigate(['/order-confirmed', this.orderId]);
|
|
||||||
|
this.quoteService.reportPayment(this.orderId, this.selectedPaymentMethod).subscribe({
|
||||||
|
next: (order) => {
|
||||||
|
this.order.set(order);
|
||||||
|
// The UI will re-render and show the 'REPORTED' state.
|
||||||
|
// We stay on this page to let the user see the "In verifica"
|
||||||
|
// status along with payment instructions.
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
console.error('Failed to report payment', err);
|
||||||
|
this.error.set('Failed to report payment. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getDisplayOrderNumber(order: any): string {
|
getDisplayOrderNumber(order: any): string {
|
||||||
|
|||||||
@@ -194,7 +194,18 @@
|
|||||||
"SHIPPING": "Shipping",
|
"SHIPPING": "Shipping",
|
||||||
"SETUP_FEE": "Setup Fee",
|
"SETUP_FEE": "Setup Fee",
|
||||||
"TOTAL": "Total",
|
"TOTAL": "Total",
|
||||||
"LOADING": "Loading order details..."
|
"LOADING": "Loading order details...",
|
||||||
|
"METHOD_TWINT": "TWINT",
|
||||||
|
"METHOD_BANK": "Bank Transfer / QR",
|
||||||
|
"STATUS_REPORTED_TITLE": "Payment Reported",
|
||||||
|
"STATUS_REPORTED_DESC": "We are verifying your transaction. Your order will move to production as soon as the payment is confirmed.",
|
||||||
|
"IN_VERIFICATION": "Verifying Payment"
|
||||||
|
},
|
||||||
|
"TRACKING": {
|
||||||
|
"STEP_PENDING": "Pending",
|
||||||
|
"STEP_REPORTED": "Verifying",
|
||||||
|
"STEP_PRODUCTION": "Production",
|
||||||
|
"STEP_SHIPPED": "Shipped"
|
||||||
},
|
},
|
||||||
"ORDER_CONFIRMED": {
|
"ORDER_CONFIRMED": {
|
||||||
"TITLE": "Order Confirmed",
|
"TITLE": "Order Confirmed",
|
||||||
|
|||||||
@@ -267,7 +267,16 @@
|
|||||||
"TOTAL": "Totale",
|
"TOTAL": "Totale",
|
||||||
"LOADING": "Caricamento dettagli ordine...",
|
"LOADING": "Caricamento dettagli ordine...",
|
||||||
"METHOD_TWINT": "TWINT",
|
"METHOD_TWINT": "TWINT",
|
||||||
"METHOD_BANK": "Fattura QR / Bonifico"
|
"METHOD_BANK": "Fattura QR / Bonifico",
|
||||||
|
"STATUS_REPORTED_TITLE": "Abbiamo ricevuto la tua segnalazione",
|
||||||
|
"STATUS_REPORTED_DESC": "Stiamo verificando la transazione. Il tuo ordine passerà in produzione non appena l'accredito sarà confermato.",
|
||||||
|
"IN_VERIFICATION": "Pagamento in verifica"
|
||||||
|
},
|
||||||
|
"TRACKING": {
|
||||||
|
"STEP_PENDING": "In attesa",
|
||||||
|
"STEP_REPORTED": "In verifica",
|
||||||
|
"STEP_PRODUCTION": "In Produzione",
|
||||||
|
"STEP_SHIPPED": "Spedito"
|
||||||
},
|
},
|
||||||
"ORDER_CONFIRMED": {
|
"ORDER_CONFIRMED": {
|
||||||
"TITLE": "Ordine Confermato",
|
"TITLE": "Ordine Confermato",
|
||||||
|
|||||||
Reference in New Issue
Block a user