StorageBinService.java
package com.v1rex.liftnexus.storagebin.service;
import com.v1rex.liftnexus.storagebin.domain.StorageBin;
import com.v1rex.liftnexus.storagebin.dto.StorageBinRequest;
import com.v1rex.liftnexus.storagebin.dto.StorageBinResponse;
import com.v1rex.liftnexus.storagebin.exception.StorageBinCodeExistsException;
import com.v1rex.liftnexus.storagebin.exception.StorageBinNotFoundException;
import com.v1rex.liftnexus.storagebin.mapper.StorageBinMapper;
import com.v1rex.liftnexus.storagebin.repository.StorageBinRepository;
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 for {@link StorageBin} domain operations.
*
* <p>This class provides a clear separation between <b>external API</b> methods (returning DTOs to
* controllers) and <b>internal domain</b> methods (returning entities to other services or the
* Timefold solver). This dual-boundary pattern ensures that external clients receive decoupled
* response objects, while internal consumers have full access to the domain model for complex
* operations like constraint-based optimisation.
*
* <p>All public methods are {@link Transactional @Transactional} to guarantee data consistency.
*
* @see StorageBinRepository
* @see StorageBinMapper
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class StorageBinService {
private final StorageBinRepository storageBinRepository;
private final StorageBinMapper storageBinMapper;
// =====================================================================
// EXTERNAL API BOUNDARY (Returns DTOs to Controllers)
// =====================================================================
/**
* Creates a new storage bin and returns its DTO representation.
*
* <p>Before persisting, this method validates that the supplied {@code binCode} is unique. If a
* storage bin with the same code already exists, a {@link StorageBinCodeExistsException} is
* thrown.
*
* @param request the input data containing the bin code and its spatial coordinates
* @return a {@link StorageBinResponse} representing the newly persisted storage bin
* @throws StorageBinCodeExistsException if a storage bin with the given {@code binCode} already
* exists in the database
*/
@Transactional
public StorageBinResponse createStorageBin(StorageBinRequest request) {
log.info(
"Creating storage bin with code: {} at [X:{}, Y:{}, Z:{}]",
request.binCode(),
request.coordinate().x(),
request.coordinate().y(),
request.coordinate().z());
if (storageBinRepository.existsByBinCode(request.binCode())) {
throw new StorageBinCodeExistsException(request.binCode());
}
StorageBin storageBin = storageBinMapper.toEntity(request);
StorageBin savedBin = storageBinRepository.save(storageBin);
log.info(
"Successfully created storage bin with Id: {}, code: {}",
savedBin.getId(),
savedBin.getBinCode());
return storageBinMapper.toResponse(savedBin);
}
/**
* Retrieves a storage bin by its unique identifier and returns its DTO representation.
*
* <p>If no storage bin exists with the given {@code id}, a {@link StorageBinNotFoundException} is
* thrown.
*
* @param id the storage bin's primary key
* @return a {@link StorageBinResponse} for the matching storage bin
* @throws StorageBinNotFoundException if no storage bin is found for the given {@code id}
* @see #findEntityById(Long)
*/
@Transactional(readOnly = true)
public StorageBinResponse findById(Long id) {
return storageBinMapper.toResponse(findEntityById(id));
}
/**
* Retrieves a paginated list of all storage bins, mapped to their DTO representations.
*
* @param pageable pagination and sorting parameters
* @return a {@link Page} of {@link StorageBinResponse} objects
* @see #findAllEntities(Pageable)
*/
@Transactional(readOnly = true)
public Page<StorageBinResponse> findAll(Pageable pageable) {
return findAllEntities(pageable).map(storageBinMapper::toResponse);
}
// =====================================================================
// INTERNAL DOMAIN BOUNDARY (Returns Entities to other Services/Timefold)
// =====================================================================
/**
* Finds a {@link StorageBin} entity by its identifier.
*
* <p>This is an <b>internal</b> method intended for use by other services or the Timefold solver
* that require access to the full domain model. It throws a {@link StorageBinNotFoundException}
* when the entity is not found.
*
* @param id the storage bin's primary key
* @return the {@link StorageBin} entity
* @throws StorageBinNotFoundException if no storage bin exists for the given {@code id}
*/
@Transactional(readOnly = true)
public StorageBin findEntityById(Long id) {
log.info("Fetching storage bin entity with id: {}", id);
return storageBinRepository
.findById(id)
.orElseThrow(
() -> {
log.warn("Storage bin with id: {} not found.", id);
return new StorageBinNotFoundException(id);
});
}
/**
* Retrieves a paginated list of all {@link StorageBin} entities.
*
* <p>This is an <b>internal</b> method that returns full domain objects, suitable for batch
* processing or solver input.
*
* @param pageable pagination and sorting parameters
* @return a {@link Page} of {@link StorageBin} entities
*/
@Transactional(readOnly = true)
public Page<StorageBin> findAllEntities(Pageable pageable) {
log.info("Fetching all managed storage bin entities with pagination");
return storageBinRepository.findAll(pageable);
}
/**
* Retrieves all {@link StorageBin} entities without pagination.
*
* <p>This is an <b>internal</b> method that should be used with care when the total number of
* bins is expected to be small, or when the caller intentionally loads the full collection (e.g.,
* seeding the Timefold solver).
*
* @return an unmodifiable-style {@link List} of all {@link StorageBin} entities
*/
@Transactional(readOnly = true)
public List<StorageBin> findAllEntities() {
log.info("Fetching all managed storage bin entities without pagination");
return storageBinRepository.findAll();
}
}