From c1652798b4af9fcedefd829bf145b156c3c5fbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joe=20K=C3=BCng?= Date: Mon, 23 Feb 2026 18:56:24 +0100 Subject: [PATCH] feat(back-end front-end): new UX for --- .gitignore | 5 + .../controller/OrderController.java | 26 +- .../com/printcalculator/dto/OrderDto.java | 8 + .../com/printcalculator/entity/Payment.java | 11 + .../event/PaymentReportedEvent.java | 25 ++ .../event/listener/OrderEmailListener.java | 30 ++ .../repository/PaymentRepository.java | 2 + .../printcalculator/service/OrderService.java | 12 +- .../service/PaymentService.java | 74 +++++ .../service/ProfileManager.java | 46 ++- .../email/SmtpEmailNotificationService.java | 8 + .../resources/application-local.properties | 2 + .../src/main/resources/application.properties | 1 + .../src/main/resources/templates/invoice.html | 280 ++++++++++++------ .../listener/OrderEmailListenerTest.java | 131 ++++++++ .../SmtpEmailNotificationServiceTest.java | 83 ++++++ db.sql | 3 +- docker-compose.yml | 40 --- .../services/quote-estimator.service.ts | 7 + .../order-confirmed.component.html | 31 +- .../order-confirmed.component.scss | 97 ++++++ .../order-confirmed.component.ts | 4 +- .../features/payment/payment.component.html | 14 +- .../app/features/payment/payment.component.ts | 17 +- frontend/src/assets/i18n/en.json | 13 +- frontend/src/assets/i18n/it.json | 11 +- 26 files changed, 839 insertions(+), 142 deletions(-) create mode 100644 backend/src/main/java/com/printcalculator/event/PaymentReportedEvent.java create mode 100644 backend/src/main/java/com/printcalculator/service/PaymentService.java create mode 100644 backend/src/main/resources/application-local.properties create mode 100644 backend/src/test/java/com/printcalculator/event/listener/OrderEmailListenerTest.java create mode 100644 backend/src/test/java/com/printcalculator/service/email/SmtpEmailNotificationServiceTest.java diff --git a/.gitignore b/.gitignore index bb7e6b2..9381358 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,8 @@ target/ build/ .gradle/ .mvn/ + +./storage_orders +./storage_quotes +storage_orders +storage_quotes \ No newline at end of file diff --git a/backend/src/main/java/com/printcalculator/controller/OrderController.java b/backend/src/main/java/com/printcalculator/controller/OrderController.java index 3bfc8a9..bae5d52 100644 --- a/backend/src/main/java/com/printcalculator/controller/OrderController.java +++ b/backend/src/main/java/com/printcalculator/controller/OrderController.java @@ -5,6 +5,7 @@ import com.printcalculator.entity.*; import com.printcalculator.repository.*; import com.printcalculator.service.InvoicePdfRenderingService; import com.printcalculator.service.OrderService; +import com.printcalculator.service.PaymentService; import com.printcalculator.service.QrBillService; import com.printcalculator.service.StorageService; import com.printcalculator.service.TwintPaymentService; @@ -43,6 +44,8 @@ public class OrderController { private final InvoicePdfRenderingService invoiceService; private final QrBillService qrBillService; private final TwintPaymentService twintPaymentService; + private final PaymentService paymentService; + private final PaymentRepository paymentRepo; public OrderController(OrderService orderService, @@ -54,7 +57,9 @@ public class OrderController { StorageService storageService, InvoicePdfRenderingService invoiceService, QrBillService qrBillService, - TwintPaymentService twintPaymentService) { + TwintPaymentService twintPaymentService, + PaymentService paymentService, + PaymentRepository paymentRepo) { this.orderService = orderService; this.orderRepo = orderRepo; this.orderItemRepo = orderItemRepo; @@ -65,6 +70,8 @@ public class OrderController { this.invoiceService = invoiceService; this.qrBillService = qrBillService; this.twintPaymentService = twintPaymentService; + this.paymentService = paymentService; + this.paymentRepo = paymentRepo; } @@ -122,6 +129,17 @@ public class OrderController { .orElse(ResponseEntity.notFound().build()); } + @PostMapping("/{orderId}/payments/report") + @Transactional + public ResponseEntity reportPayment( + @PathVariable UUID orderId, + @RequestBody Map payload + ) { + String method = payload.get("method"); + paymentService.reportPayment(orderId, method); + return getOrder(orderId); + } + @GetMapping("/{orderId}/invoice") public ResponseEntity getInvoice(@PathVariable UUID orderId) { Order order = orderRepo.findById(orderId) @@ -251,6 +269,12 @@ public class OrderController { 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.setBillingCustomerType(order.getBillingCustomerType()); diff --git a/backend/src/main/java/com/printcalculator/dto/OrderDto.java b/backend/src/main/java/com/printcalculator/dto/OrderDto.java index eee6ef0..eccd46c 100644 --- a/backend/src/main/java/com/printcalculator/dto/OrderDto.java +++ b/backend/src/main/java/com/printcalculator/dto/OrderDto.java @@ -9,6 +9,8 @@ public class OrderDto { private UUID id; private String orderNumber; private String status; + private String paymentStatus; + private String paymentMethod; private String customerEmail; private String customerPhone; private String billingCustomerType; @@ -34,6 +36,12 @@ public class OrderDto { public String getStatus() { return 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 void setCustomerEmail(String customerEmail) { this.customerEmail = customerEmail; } diff --git a/backend/src/main/java/com/printcalculator/entity/Payment.java b/backend/src/main/java/com/printcalculator/entity/Payment.java index 73b5a31..4b13a93 100644 --- a/backend/src/main/java/com/printcalculator/entity/Payment.java +++ b/backend/src/main/java/com/printcalculator/entity/Payment.java @@ -52,6 +52,9 @@ public class Payment { @Column(name = "initiated_at", nullable = false) private OffsetDateTime initiatedAt; + @Column(name = "reported_at") + private OffsetDateTime reportedAt; + @Column(name = "received_at") private OffsetDateTime receivedAt; @@ -135,6 +138,14 @@ public class Payment { this.initiatedAt = initiatedAt; } + public OffsetDateTime getReportedAt() { + return reportedAt; + } + + public void setReportedAt(OffsetDateTime reportedAt) { + this.reportedAt = reportedAt; + } + public OffsetDateTime getReceivedAt() { return receivedAt; } diff --git a/backend/src/main/java/com/printcalculator/event/PaymentReportedEvent.java b/backend/src/main/java/com/printcalculator/event/PaymentReportedEvent.java new file mode 100644 index 0000000..51f378d --- /dev/null +++ b/backend/src/main/java/com/printcalculator/event/PaymentReportedEvent.java @@ -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; + } +} diff --git a/backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java b/backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java index d600847..bd7761d 100644 --- a/backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java +++ b/backend/src/main/java/com/printcalculator/event/listener/OrderEmailListener.java @@ -1,7 +1,9 @@ package com.printcalculator.event.listener; import com.printcalculator.entity.Order; +import com.printcalculator.entity.Payment; import com.printcalculator.event.OrderCreatedEvent; +import com.printcalculator.event.PaymentReportedEvent; import com.printcalculator.service.email.EmailNotificationService; import lombok.RequiredArgsConstructor; 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) { Map templateData = new HashMap<>(); templateData.put("customerName", order.getCustomer().getFirstName()); @@ -64,6 +79,21 @@ public class OrderEmailListener { ); } + private void sendPaymentReportedEmail(Order order) { + Map 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) { Map templateData = new HashMap<>(); templateData.put("customerName", order.getCustomer().getFirstName() + " " + order.getCustomer().getLastName()); diff --git a/backend/src/main/java/com/printcalculator/repository/PaymentRepository.java b/backend/src/main/java/com/printcalculator/repository/PaymentRepository.java index 1cd5fca..38cd466 100644 --- a/backend/src/main/java/com/printcalculator/repository/PaymentRepository.java +++ b/backend/src/main/java/com/printcalculator/repository/PaymentRepository.java @@ -3,7 +3,9 @@ package com.printcalculator.repository; import com.printcalculator.entity.Payment; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.Optional; import java.util.UUID; public interface PaymentRepository extends JpaRepository { + Optional findByOrder_Id(UUID orderId); } \ No newline at end of file diff --git a/backend/src/main/java/com/printcalculator/service/OrderService.java b/backend/src/main/java/com/printcalculator/service/OrderService.java index ad965af..b789191 100644 --- a/backend/src/main/java/com/printcalculator/service/OrderService.java +++ b/backend/src/main/java/com/printcalculator/service/OrderService.java @@ -36,6 +36,7 @@ public class OrderService { private final InvoicePdfRenderingService invoiceService; private final QrBillService qrBillService; private final ApplicationEventPublisher eventPublisher; + private final PaymentService paymentService; public OrderService(OrderRepository orderRepo, OrderItemRepository orderItemRepo, @@ -45,7 +46,8 @@ public class OrderService { StorageService storageService, InvoicePdfRenderingService invoiceService, QrBillService qrBillService, - ApplicationEventPublisher eventPublisher) { + ApplicationEventPublisher eventPublisher, + PaymentService paymentService) { this.orderRepo = orderRepo; this.orderItemRepo = orderItemRepo; this.quoteSessionRepo = quoteSessionRepo; @@ -55,6 +57,7 @@ public class OrderService { this.invoiceService = invoiceService; this.qrBillService = qrBillService; this.eventPublisher = eventPublisher; + this.paymentService = paymentService; } @Transactional @@ -198,9 +201,12 @@ public class OrderService { // Generate Invoice and QR Bill generateAndSaveDocuments(order, savedItems); - + Order savedOrder = orderRepo.save(order); - + + // ALWAYS initialize payment as PENDING + paymentService.getOrCreatePaymentForOrder(savedOrder, "OTHER"); + eventPublisher.publishEvent(new OrderCreatedEvent(this, savedOrder)); return savedOrder; diff --git a/backend/src/main/java/com/printcalculator/service/PaymentService.java b/backend/src/main/java/com/printcalculator/service/PaymentService.java new file mode 100644 index 0000000..2588523 --- /dev/null +++ b/backend/src/main/java/com/printcalculator/service/PaymentService.java @@ -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 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; + } +} diff --git a/backend/src/main/java/com/printcalculator/service/ProfileManager.java b/backend/src/main/java/com/printcalculator/service/ProfileManager.java index 9df3f45..3737d43 100644 --- a/backend/src/main/java/com/printcalculator/service/ProfileManager.java +++ b/backend/src/main/java/com/printcalculator/service/ProfileManager.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.ArrayList; import java.util.Iterator; import java.util.Optional; import java.util.logging.Logger; @@ -17,12 +18,15 @@ import java.util.stream.Stream; import java.util.Map; import java.util.HashMap; import java.util.List; +import java.util.LinkedHashSet; +import java.util.Set; @Service public class ProfileManager { private static final Logger logger = Logger.getLogger(ProfileManager.class.getName()); private final String profilesRoot; + private final Path resolvedProfilesRoot; private final ObjectMapper mapper; private final Map profileAliases; @@ -32,6 +36,8 @@ public class ProfileManager { this.mapper = mapper; this.profileAliases = new HashMap<>(); initializeAliases(); + this.resolvedProfilesRoot = resolveProfilesRoot(profilesRoot); + logger.info("Profiles root configured as '" + this.profilesRoot + "', resolved to '" + this.resolvedProfilesRoot + "'"); } private void initializeAliases() { @@ -55,13 +61,18 @@ public class ProfileManager { public ObjectNode getMergedProfile(String profileName, String type) throws IOException { Path profilePath = findProfileFile(profileName, type); 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); return resolveInheritance(profilePath); } 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 String resolvedName = profileAliases.getOrDefault(name, name); @@ -69,7 +80,7 @@ public class ProfileManager { // collisions across vendors/profile families with same filename. String filename = toJsonFilename(resolvedName); - try (Stream stream = Files.walk(Paths.get(profilesRoot))) { + try (Stream stream = Files.walk(resolvedProfilesRoot)) { List candidates = stream .filter(p -> p.getFileName().toString().equals(filename)) .sorted() @@ -95,6 +106,37 @@ public class ProfileManager { } } + private Path resolveProfilesRoot(String configuredRoot) { + Set 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 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 { // 1. Load current JsonNode currentNode = mapper.readTree(currentPath.toFile()); diff --git a/backend/src/main/java/com/printcalculator/service/email/SmtpEmailNotificationService.java b/backend/src/main/java/com/printcalculator/service/email/SmtpEmailNotificationService.java index 3e0987b..c10fcce 100644 --- a/backend/src/main/java/com/printcalculator/service/email/SmtpEmailNotificationService.java +++ b/backend/src/main/java/com/printcalculator/service/email/SmtpEmailNotificationService.java @@ -24,8 +24,16 @@ public class SmtpEmailNotificationService implements EmailNotificationService { @Value("${app.mail.from}") private String fromAddress; + @Value("${app.mail.enabled:true}") + private boolean mailEnabled; + @Override public void sendEmail(String to, String subject, String templateName, Map 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); try { diff --git a/backend/src/main/resources/application-local.properties b/backend/src/main/resources/application-local.properties new file mode 100644 index 0000000..45c5593 --- /dev/null +++ b/backend/src/main/resources/application-local.properties @@ -0,0 +1,2 @@ +app.mail.enabled=false +app.mail.admin.enabled=false diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index ddaa42e..424defb 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -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} # Application Mail Settings +app.mail.enabled=${APP_MAIL_ENABLED:true} app.mail.from=${APP_MAIL_FROM:${MAIL_USERNAME:noreply@printcalculator.local}} app.mail.admin.enabled=${APP_MAIL_ADMIN_ENABLED:true} app.mail.admin.address=${APP_MAIL_ADMIN_ADDRESS:admin@printcalculator.local} diff --git a/backend/src/main/resources/templates/invoice.html b/backend/src/main/resources/templates/invoice.html index 657d5d7..87677d3 100644 --- a/backend/src/main/resources/templates/invoice.html +++ b/backend/src/main/resources/templates/invoice.html @@ -3,17 +3,18 @@