OrderService.java

package cz.vsb.crm.service;

import com.google.common.base.Preconditions;
import cz.vsb.crm.dto.OrderLineRequest;
import cz.vsb.crm.dto.OrderLineResponse;
import cz.vsb.crm.dto.OrderProductView;
import cz.vsb.crm.dto.OrderRequest;
import cz.vsb.crm.dto.OrderResponse;
import cz.vsb.crm.exception.BusinessRuleException;
import cz.vsb.crm.exception.ResourceNotFoundException;
import cz.vsb.crm.model.Order;
import cz.vsb.crm.model.OrderProduct;
import cz.vsb.crm.model.Product;
import cz.vsb.crm.repository.AccountRepository;
import cz.vsb.crm.repository.LeadRepository;
import cz.vsb.crm.repository.OrderProductRepository;
import cz.vsb.crm.repository.OrderRepository;
import cz.vsb.crm.repository.ProductRepository;
import cz.vsb.crm.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.util.List;

@Service
@RequiredArgsConstructor
@Transactional
public class OrderService {

    private final OrderRepository orderRepository;
    private final OrderProductRepository orderProductRepository;
    private final AccountRepository accountRepository;
    private final LeadRepository leadRepository;
    private final UserRepository userRepository;
    private final ProductRepository productRepository;

    @Transactional(readOnly = true)
    public List<OrderResponse> findAll() {
        return orderRepository.findAll().stream().map(OrderResponse::from).toList();
    }

    @Transactional(readOnly = true)
    public OrderResponse findById(Long id) {
        return OrderResponse.from(get(id));
    }

    public OrderResponse create(OrderRequest request) {
        return OrderResponse.from(save(new Order(), request));
    }

    public OrderResponse update(Long id, OrderRequest request) {
        return OrderResponse.from(save(get(id), request));
    }

    public void delete(Long id) {
        Order order = get(id);
        order.getOrderProducts().forEach(line -> restock(line.getProduct(), line.getQuantity()));
        orderRepository.delete(order);
    }

    @Transactional(readOnly = true)
    public List<OrderLineResponse> getProducts(Long id) {
        return get(id).getOrderProducts().stream().map(OrderLineResponse::from).toList();
    }

    @Transactional(readOnly = true)
    public List<OrderProductView> getAllProductLines() {
        return orderProductRepository.findAll().stream().map(OrderProductView::from).toList();
    }

    private Order get(Long id) {
        return orderRepository.findById(id)
                .orElseThrow(() -> new ResourceNotFoundException("Order", id));
    }

    private Order save(Order order, OrderRequest request) {
        order.setAccount(accountRepository.findById(request.accountId())
                .orElseThrow(() -> new ResourceNotFoundException("Account", request.accountId())));
        order.setLead(request.leadId() == null ? null
                : leadRepository.findById(request.leadId())
                        .orElseThrow(() -> new ResourceNotFoundException("Lead", request.leadId())));
        order.setCreatedBy(request.createdById() == null ? null
                : userRepository.findById(request.createdById())
                        .orElseThrow(() -> new ResourceNotFoundException("User", request.createdById())));
        if (request.status() != null) {
            order.setStatus(request.status());
        }
        if (request.orderType() != null) {
            order.setOrderType(request.orderType());
        }
        if (request.orderDate() != null) {
            order.setOrderDate(request.orderDate());
        }

        order.getOrderProducts().forEach(line -> restock(line.getProduct(), line.getQuantity()));
        order.getOrderProducts().clear();
        orderRepository.saveAndFlush(order);

        BigDecimal total = BigDecimal.ZERO;
        if (request.products() != null) {
            for (OrderLineRequest line : request.products()) {
                Product product = productRepository.findById(line.productId())
                        .orElseThrow(() -> new ResourceNotFoundException("Product", line.productId()));

                int quantity = line.quantity();
                int available = product.getStock() != null ? product.getStock() : 0;
                if (quantity > available) {
                    throw new BusinessRuleException("Insufficient stock for " + product.getName()
                            + ". Available: " + available + ", Requested: " + quantity);
                }
                product.setStock(available - quantity);

                BigDecimal unitPrice = line.unitPrice() != null ? line.unitPrice()
                        : (product.getPrice() != null ? product.getPrice() : BigDecimal.ZERO);
                BigDecimal subtotal = unitPrice.multiply(BigDecimal.valueOf(quantity));

                OrderProduct orderProduct = new OrderProduct();
                orderProduct.setOrder(order);
                orderProduct.setProduct(product);
                orderProduct.setQuantity(quantity);
                orderProduct.setUnitPrice(unitPrice);
                orderProduct.setSubtotal(subtotal);
                order.getOrderProducts().add(orderProduct);

                total = total.add(subtotal);
            }
        }
        order.setTotalAmount(total);
        return orderRepository.save(order);
    }

    private void restock(Product product, Integer quantity) {
        Preconditions.checkNotNull(product, "order line product must not be null");
        Preconditions.checkNotNull(quantity, "order line quantity must not be null");
        int current = product.getStock() != null ? product.getStock() : 0;
        product.setStock(current + quantity);
    }
}