bilhej/backend/src/main/java/se/bilhalsning/service/OrderService.java
Hermes Agent 7d6d541354
Some checks failed
CI / Lint, type check, unit tests, coverage (pull_request) Successful in 3m26s
CI / E2E browser tests (pull_request) Failing after 1m53s
fix(payment): persist amountPaid on payment confirmation
confirmGuestPayment and confirmPayment set the order status to
PROCESSING but never called order.setAmountPaid(...). As a result
amountPaid always read null even after payment, blocking finance
reconciliation against the Swish payout report.

The frontend test mock (GuestPaymentRedirect.spec.ts) expected
amountPaid: 49 but the real backend returned null.

Changes:
  - OrderService: inject app.payment.letter-price (default 49) via
    @Value and set order.setAmountPaid(BigDecimal.valueOf(letterPrice))
    before save in both confirmGuestPayment and confirmPayment
  - OrderServiceTest: assert amountPaid == 49 after both confirm paths

Verified: ./gradlew :backend:test — BUILD SUCCESSFUL

Closes #20
2026-07-18 11:38:10 +00:00

135 lines
5 KiB
Java

package se.bilhalsning.service;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import se.bilhalsning.entity.Order;
import se.bilhalsning.entity.OrderStatus;
import se.bilhalsning.exception.InvalidOrderStateException;
import se.bilhalsning.exception.OrderNotFoundException;
import se.bilhalsning.repository.OrderRepository;
import java.math.BigDecimal;
import java.util.List;
import java.util.UUID;
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final OrderNotificationService orderNotificationService;
@Value("${app.payment.letter-price:49}")
private int letterPrice = 49;
public Order createOrder(UUID userId, String plate, String letterText) {
Order order = new Order();
order.setUserId(userId);
order.setPlate(plate.toUpperCase().trim());
order.setLetterText(letterText);
order.setStatus(OrderStatus.PENDING_PAYMENT);
return orderRepository.save(order);
}
/**
* Guest checkout — creates an order with no registered user. The
* {@code guestToken} is generated in {@link Order#onCreate()} so the
* caller does not need to handle token creation logic. Returned
* order's {@link Order#getGuestToken()} is the only credential the
* client receives.
*/
public Order createGuestOrder(String plate, String letterText, String email) {
Order order = new Order();
// userId stays null — guest order. guestToken auto-generated in PrePersist.
order.setGuestEmail(email == null ? null : email.trim().toLowerCase());
order.setPlate(plate.toUpperCase().trim());
order.setLetterText(letterText);
order.setStatus(OrderStatus.PENDING_PAYMENT);
return orderRepository.save(order);
}
/**
* Guest-path order lookup by opaque token. Never exposes user-owned
* orders via this path: if a guest token resolves to an order with a
* non-null {@code userId} (corrupted data, manual SQL insert, etc.),
* treat as not-found rather than leak the order.
*/
public Order getOrderByGuestToken(UUID guestToken) {
Order order = orderRepository.findByGuestToken(guestToken)
.orElseThrow(() -> new OrderNotFoundException(guestToken));
if (order.getUserId() != null) {
// Token points at a user-owned order — refuse to serve it.
throw new OrderNotFoundException(guestToken);
}
return order;
}
/**
* Honor-system payment confirmation for guest orders. Mirrors
* {@link #confirmPayment(UUID, UUID)} but authenticates via the
* guest token instead of {@code userId}.
*/
public Order confirmGuestPayment(UUID guestToken) {
Order order = getOrderByGuestToken(guestToken);
if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
throw new InvalidOrderStateException(
"Beställningen kan inte ändras i detta tillstånd");
}
order.setStatus(OrderStatus.PROCESSING);
order.setAmountPaid(BigDecimal.valueOf(letterPrice));
Order saved = orderRepository.save(order);
orderNotificationService.notifyOrderProcessing(saved);
return saved;
}
public List<Order> getOrdersByUserId(UUID userId) {
return orderRepository.findByUserIdOrderByCreatedAtDesc(userId);
}
public Order getOrderById(UUID id) {
return orderRepository.findById(id)
.orElseThrow(() -> new OrderNotFoundException(id));
}
public List<Order> getAllOrders() {
return orderRepository.findAllByOrderByCreatedAtDesc();
}
public Order confirmPayment(UUID orderId, UUID userId) {
Order order = requirePendingOwnedBy(orderId, userId);
order.setStatus(OrderStatus.PROCESSING);
order.setAmountPaid(BigDecimal.valueOf(letterPrice));
Order saved = orderRepository.save(order);
orderNotificationService.notifyOrderProcessing(saved);
return saved;
}
public Order cancelOrder(UUID orderId, UUID userId) {
Order order = requirePendingOwnedBy(orderId, userId);
order.setStatus(OrderStatus.CANCELLED);
return orderRepository.save(order);
}
public Order updatePendingOrder(UUID orderId, UUID userId, String letterText) {
Order order = requirePendingOwnedBy(orderId, userId);
order.setLetterText(letterText);
return orderRepository.save(order);
}
private Order requirePendingOwnedBy(UUID orderId, UUID userId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
if (!order.getUserId().equals(userId)) {
throw new OrderNotFoundException(orderId);
}
if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
throw new InvalidOrderStateException(
"Beställningen kan inte ändras i detta tillstånd");
}
return order;
}
}