LoadUnitService.java
package com.v1rex.liftnexus.loadunit.service;
import com.v1rex.liftnexus.loadunit.domain.LoadUnit;
import com.v1rex.liftnexus.loadunit.domain.LoadUnitStatus;
import com.v1rex.liftnexus.loadunit.dto.LoadUnitRequest;
import com.v1rex.liftnexus.loadunit.dto.LoadUnitResponse;
import com.v1rex.liftnexus.loadunit.exception.LoadUnitNotFoundException;
import com.v1rex.liftnexus.loadunit.exception.LoadUnitTrackingCodeExistsException;
import com.v1rex.liftnexus.loadunit.mapper.LoadUnitMapper;
import com.v1rex.liftnexus.loadunit.repository.LoadUnitRepository;
import com.v1rex.liftnexus.storagebin.domain.StorageBin;
import com.v1rex.liftnexus.storagebin.service.StorageBinService;
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 for managing {@link LoadUnit} entities.
*
* <p>This class provides two tiers of access:
*
* <ul>
* <li><b>External API Boundary</b> – Methods that return DTOs ({@link LoadUnitResponse}) to
* controllers and external callers. These methods handle validation, business logic, and
* mapping.
* <li><b>Internal Domain Boundary</b> – Methods that return domain entities ({@link LoadUnit})
* for use by other services, internal components, or the Timefold solver, bypassing DTO
* conversion.
* </ul>
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class LoadUnitService {
private final LoadUnitRepository loadUnitRepository;
private final LoadUnitMapper loadUnitMapper;
private final StorageBinService storageBinService;
// =====================================================================
// EXTERNAL API BOUNDARY (Returns DTOs to Controllers)
// =====================================================================
/**
* Creates a new load unit and persists it to the database.
*
* <p>Before persisting, this method validates that the provided {@code trackingCode} is unique.
* If a storage bin ID is supplied, the load unit will be assigned to the referenced bin (the bin
* must already exist).
*
* @param request the DTO containing the load unit details (tracking code, weight, optional
* storage bin ID, etc.)
* @return a {@link LoadUnitResponse} representing the newly created and saved load unit
* @throws LoadUnitTrackingCodeExistsException if a load unit with the same tracking code already
* exists
*/
@Transactional
public LoadUnitResponse createLoadUnit(LoadUnitRequest request) {
log.info(
"Creating load unit with tracking code: {} and weight: {}kg",
request.trackingCode(),
request.weightKg());
if (loadUnitRepository.existsByTrackingCode(request.trackingCode())) {
log.warn(
"Creation failed: Load unit with tracking code {} already exists",
request.trackingCode());
throw new LoadUnitTrackingCodeExistsException(request.trackingCode());
}
StorageBin assignedBin = null;
if (request.currentStorageBinId() != null) {
assignedBin = storageBinService.findEntityById(request.currentStorageBinId());
}
LoadUnit loadUnit = loadUnitMapper.toEntity(request, assignedBin);
LoadUnit savedUnit = loadUnitRepository.save(loadUnit);
log.info(
"Successfully created load unit with Id: {}, tracking code: {}",
savedUnit.getId(),
savedUnit.getTrackingCode());
return loadUnitMapper.toResponse(savedUnit);
}
/**
* Retrieves a load unit by its database ID and returns it as a DTO.
*
* @param id the primary key of the load unit
* @return a {@link LoadUnitResponse} for the matching load unit
* @throws LoadUnitNotFoundException if no load unit exists with the given {@code id}
*/
@Transactional(readOnly = true)
public LoadUnitResponse findById(Long id) {
return loadUnitMapper.toResponse(findEntityById(id));
}
/**
* Retrieves a load unit by its unique tracking code and returns it as a DTO.
*
* @param trackingCode the unique tracking code to search for
* @return a {@link LoadUnitResponse} for the matching load unit
* @throws LoadUnitNotFoundException if no load unit exists with the given {@code trackingCode}
*/
@Transactional(readOnly = true)
public LoadUnitResponse findByTrackingCode(String trackingCode) {
return loadUnitMapper.toResponse(findEntityByTrackingCode(trackingCode));
}
/**
* Returns a paginated list of all load units as DTOs.
*
* @param pageable pagination and sorting configuration
* @return a {@link Page} of {@link LoadUnitResponse}
*/
@Transactional(readOnly = true)
public Page<LoadUnitResponse> findAll(Pageable pageable) {
return findAllEntities(pageable).map(loadUnitMapper::toResponse);
}
/**
* Returns a paginated list of load units filtered by the given {@link LoadUnitStatus} as DTOs.
*
* @param status the status to filter by (e.g. {@code AVAILABLE}, {@code RESERVED}, etc.)
* @param pageable pagination and sorting configuration
* @return a {@link Page} of {@link LoadUnitResponse} matching the specified status
*/
@Transactional(readOnly = true)
public Page<LoadUnitResponse> findByStatus(LoadUnitStatus status, Pageable pageable) {
return findEntitiesByStatus(status, pageable).map(loadUnitMapper::toResponse);
}
// =====================================================================
// INTERNAL DOMAIN BOUNDARY (Returns Entities to other Services/Timefold)
// =====================================================================
/**
* Finds a load unit entity by its database ID.
*
* <p>This method is intended for internal use by other services, domain components, or the
* Timefold solver that require direct access to the domain entity rather than a DTO.
*
* @param id the primary key of the load unit
* @return the {@link LoadUnit} entity
* @throws LoadUnitNotFoundException if no load unit exists with the given {@code id}
*/
public LoadUnit findEntityById(Long id) {
return loadUnitRepository
.findById(id)
.orElseThrow(
() -> {
log.warn("Load unit with id: {} not found.", id);
return new LoadUnitNotFoundException(id);
});
}
/**
* Finds a load unit entity by its unique tracking code.
*
* <p>This method is intended for internal use by other services, domain components, or the
* Timefold solver that require direct access to the domain entity rather than a DTO.
*
* @param trackingCode the unique tracking code to search for
* @return the {@link LoadUnit} entity
* @throws LoadUnitNotFoundException if no load unit exists with the given {@code trackingCode}
*/
public LoadUnit findEntityByTrackingCode(String trackingCode) {
return loadUnitRepository
.findByTrackingCode(trackingCode)
.orElseThrow(
() -> {
log.warn("Load unit with tracking code: {} not found.", trackingCode);
return new LoadUnitNotFoundException(trackingCode);
});
}
/**
* Returns a paginated list of all load unit entities.
*
* <p>This method is intended for internal use by other services, domain components, or the
* Timefold solver that require direct access to the domain entity rather than a DTO.
*
* @param pageable pagination and sorting configuration
* @return a {@link Page} of {@link LoadUnit} entities
*/
public Page<LoadUnit> findAllEntities(Pageable pageable) {
return loadUnitRepository.findAll(pageable);
}
/**
* Returns a paginated list of load unit entities filtered by the given {@link LoadUnitStatus}.
*
* <p>This method is intended for internal use by other services, domain components, or the
* Timefold solver that require direct access to the domain entity rather than a DTO.
*
* @param status the status to filter by
* @param pageable pagination and sorting configuration
* @return a {@link Page} of {@link LoadUnit} entities matching the specified status
*/
public Page<LoadUnit> findEntitiesByStatus(LoadUnitStatus status, Pageable pageable) {
return loadUnitRepository.findByStatus(status, pageable);
}
}