TransportOrderService.java

package com.v1rex.liftnexus.transportorder.service;

import com.v1rex.liftnexus.loadunit.domain.LoadUnit;
import com.v1rex.liftnexus.loadunit.service.LoadUnitService;
import com.v1rex.liftnexus.storagebin.domain.StorageBin;
import com.v1rex.liftnexus.storagebin.service.StorageBinService;
import com.v1rex.liftnexus.transportorder.domain.TransportOrder;
import com.v1rex.liftnexus.transportorder.domain.TransportOrderStatus;
import com.v1rex.liftnexus.transportorder.dto.TransportOrderRequest;
import com.v1rex.liftnexus.transportorder.dto.TransportOrderResponse;
import com.v1rex.liftnexus.transportorder.dto.TransportOrderStatusUpdateRequest;
import com.v1rex.liftnexus.transportorder.exception.TransportOrderInvalidStateException;
import com.v1rex.liftnexus.transportorder.exception.TransportOrderNotFoundException;
import com.v1rex.liftnexus.transportorder.mapper.TransportOrderMapper;
import com.v1rex.liftnexus.transportorder.repository.TransportOrderRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * Service layer responsible for managing transport orders within the warehouse system.
 *
 * <p>Handles the full lifecycle of a transport order: creation, status transitions, assignment to
 * forklifts, and retrieval. All public methods enforce business rules such as ensuring the load
 * unit is physically present at the source bin before a transport order can be created.
 */
@Service
@Slf4j
@RequiredArgsConstructor
public class TransportOrderService {

  private final TransportOrderRepository transportOrderRepository;
  private final TransportOrderMapper transportOrderMapper;

  private final StorageBinService storageBinService;
  private final LoadUnitService loadUnitService;

  /**
   * Creates a new transport order that moves a load unit from a source storage bin to a destination
   * storage bin.
   *
   * <p>Validates that the specified load unit is currently located in the requested source bin
   * before proceeding. Throws an exception if the load unit is not present at the source location.
   *
   * @param request DTO containing the target load unit ID, source bin ID, and destination bin ID.
   * @return The persisted transport order wrapped in a response DTO.
   * @throws TransportOrderInvalidStateException if the load unit is not in the specified source
   *     bin.
   */
  @Transactional
  public TransportOrderResponse createTransportOrder(TransportOrderRequest request) {
    log.info(
        "Creating TransportOrder for LoadUnit: {} from Bin: {} to Bin: {}",
        request.targetLoadUnitId(),
        request.sourceBinId(),
        request.destinationBinId());

    // Resolve referenced entities
    LoadUnit loadUnit = loadUnitService.findEntityById(request.targetLoadUnitId());
    StorageBin sourceBin = storageBinService.findEntityById(request.sourceBinId());
    StorageBin destinationBin = storageBinService.findEntityById(request.destinationBinId());

    // Business rule: the load unit must physically reside in the source bin
    if (loadUnit.getCurrentBin() == null
        || !loadUnit.getCurrentBin().getId().equals(sourceBin.getId())) {
      throw new TransportOrderInvalidStateException(
          "LoadUnit "
              + loadUnit.getTrackingCode()
              + " is not located in the requested source bin.");
    }

    // Map request to entity and wire up the resolved domain objects
    TransportOrder order = transportOrderMapper.toEntity(request);
    order.setTargetLoadUnit(loadUnit);
    order.setSourceBin(sourceBin);
    order.setTargetBin(destinationBin);

    TransportOrder savedOrder = transportOrderRepository.save(order);

    log.info("Successfully created TransportOrder ID: {}", savedOrder.getId());
    return transportOrderMapper.toResponse(savedOrder);
  }

  /**
   * Advances (or changes) the status of an existing transport order.
   *
   * <p>Applies state-machine rules defined in {@link #checkStatusBeforeUpdate} to prevent invalid
   * transitions such as rolling back from {@code IN_PROGRESS} to {@code OPEN} or mutating a
   * completed order.
   *
   * @param id The unique identifier of the transport order to update.
   * @param request DTO carrying the desired new status.
   * @return The updated transport order wrapped in a response DTO.
   * @throws TransportOrderNotFoundException if no order exists for the given ID.
   * @throws TransportOrderInvalidStateException if the requested transition is not allowed.
   */
  @Transactional
  public TransportOrderResponse updateOrderStatus(
      Long id, TransportOrderStatusUpdateRequest request) {
    TransportOrder order = findEntityById(id);
    TransportOrderStatus currentStatus = order.getStatus();
    TransportOrderStatus newStatus = request.status();

    checkStatusBeforeUpdate(id, currentStatus, newStatus);

    order.setStatus(newStatus);

    log.info("TransportOrder {} transitioned: {} -> {}", id, currentStatus, newStatus);
    return transportOrderMapper.toResponse(order);
  }

  /**
   * Looks up a transport order by its ID and returns the response DTO.
   *
   * @param id The unique identifier of the transport order.
   * @return The transport order response DTO.
   * @throws TransportOrderNotFoundException if no order exists for the given ID.
   */
  @Transactional(readOnly = true)
  public TransportOrderResponse findById(Long id) {
    return transportOrderMapper.toResponse(findEntityById(id));
  }

  /**
   * Searches for transport orders with optional filters and pagination.
   *
   * @param status Optional status filter (may be null).
   * @param minWeight Optional minimum weight filter (may be null).
   * @param pageable Pagination and sorting information.
   * @return A page of matching transport order response DTOs.
   */
  @Transactional(readOnly = true)
  public Page<TransportOrderResponse> searchOrders(
      TransportOrderStatus status, Integer minWeight, Pageable pageable) {
    return transportOrderRepository
        .searchOrders(status, minWeight, pageable)
        .map(transportOrderMapper::toResponse);
  }

  /**
   * Retrieves the raw {@link TransportOrder} entity by its ID.
   *
   * <p>This is used internally by other service methods that need to work with the managed JPA
   * entity rather than the response DTO.
   *
   * @param id The unique identifier of the transport order.
   * @return The managed {@link TransportOrder} entity.
   * @throws TransportOrderNotFoundException if no order exists for the given ID.
   */
  @Transactional(readOnly = true)
  public TransportOrder findEntityById(Long id) {
    return transportOrderRepository
        .findById(id)
        .orElseThrow(() -> new TransportOrderNotFoundException(id));
  }

  /**
   * Returns <strong>all</strong> transport order entities without pagination.
   *
   * <p><b>Caution:</b> This method should only be used in batch/background operations where
   * fetching the full dataset is acceptable (e.g., scheduled forklift-assignment jobs). Prefer the
   * paginated {@link #searchOrders} method for user-facing features.
   *
   * @return A list of every {@link TransportOrder} in the database.
   */
  public List<TransportOrder> findAllEntities() {
    log.info("Fetching all managed transport order entities without pagination");
    return transportOrderRepository.findAll();
  }

  /**
   * Assigns forklifts to a list of transport orders and transitions them to the {@link
   * TransportOrderStatus#ASSIGNED} state.
   *
   * <p>Each order in the provided list is fetched from the database to obtain the managed entity,
   * then updated with the assigned forklift reference. The status is automatically advanced to
   * {@code ASSIGNED}.
   *
   * @param orders List of transport order entities carrying at least the ID and the desired
   *     forklift assignment.
   */
  @Transactional
  public void updateForkliftAssignments(List<TransportOrder> orders) {
    for (TransportOrder order : orders) {
      // Re-fetch to work with the managed entity within this persistence context
      TransportOrder databaseOrder = findEntityById(order.getId());

      // Update the forklift reference
      databaseOrder.setAssignedForklift(order.getAssignedForklift());

      // Automatically transition the order to ASSIGNED status
      updateOrderStatus(
          databaseOrder.getId(),
          new TransportOrderStatusUpdateRequest(TransportOrderStatus.ASSIGNED));
    }
  }

  /**
   * Enforces the transport-order state machine rules to prevent invalid status transitions.
   *
   * <p>Currently enforced rules:
   *
   * <ul>
   *   <li>A {@code COMPLETED} order can never be changed.
   *   <li>An {@code IN_PROGRESS} order cannot be rolled back to {@code OPEN}.
   *   <li>An {@code ASSIGNED} order cannot be rolled back to {@code OPEN}.
   * </ul>
   *
   * @param id The transport order ID (used only in error messages).
   * @param currentStatus The current status of the order.
   * @param newStatus The desired new status.
   * @throws TransportOrderInvalidStateException if the transition is forbidden.
   */
  private void checkStatusBeforeUpdate(
      Long id, TransportOrderStatus currentStatus, TransportOrderStatus newStatus) {
    if (currentStatus == TransportOrderStatus.COMPLETED) {
      throw new TransportOrderInvalidStateException(
          "Cannot update TransportOrder " + id + " because it is already COMPLETED.");
    }
    if (currentStatus == TransportOrderStatus.IN_PROGRESS
        && newStatus == TransportOrderStatus.OPEN) {
      throw new TransportOrderInvalidStateException(
          "Cannot roll back TransportOrder " + id + " from ACTIVE to OPEN.");
    }
    if (currentStatus == TransportOrderStatus.ASSIGNED && newStatus == TransportOrderStatus.OPEN) {
      throw new TransportOrderInvalidStateException(
          "Cannot roll back TransportOrder " + id + " from ASSIGNED to OPEN.");
    }
  }
}