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
135 lines
5 KiB
Java
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;
|
|
}
|
|
}
|