InventoryItemServiceImpl.java
package com.smartsupplypro.inventory.service.impl;
import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.smartsupplypro.inventory.dto.InventoryItemDTO;
import com.smartsupplypro.inventory.enums.StockChangeReason;
import com.smartsupplypro.inventory.mapper.InventoryItemMapper;
import com.smartsupplypro.inventory.model.InventoryItem;
import com.smartsupplypro.inventory.repository.InventoryItemRepository;
import com.smartsupplypro.inventory.service.InventoryItemService;
import com.smartsupplypro.inventory.service.impl.inventory.InventoryItemAuditHelper;
import com.smartsupplypro.inventory.service.impl.inventory.InventoryItemValidationHelper;
import static com.smartsupplypro.inventory.validation.InventoryItemValidator.assertFinalQuantityNonNegative;
import static com.smartsupplypro.inventory.validation.InventoryItemValidator.assertPriceValid;
import lombok.RequiredArgsConstructor;
/**
* Service implementation for inventory item lifecycle management with audit trails.
*
* <p>Delegates to specialized helpers for validation and audit logging while maintaining
* the original {@link InventoryItemService} interface contract.
*
* <p><strong>Delegation Strategy</strong>:
* <ul>
* <li>{@link InventoryItemValidationHelper} - Validation, supplier checks, server field population</li>
* <li>{@link InventoryItemAuditHelper} - Stock history audit trail logging</li>
* </ul>
*
* <p><strong>Business Rules</strong>:
* <ul>
* <li>Uniqueness: No duplicate name+price combinations</li>
* <li>Positive prices: Unit price must be > 0</li>
* <li>Non-negative quantities: Final quantity cannot be negative after adjustment</li>
* <li>Supplier validation: Referenced supplier must exist</li>
* </ul>
*
* @author Smart Supply Pro Development Team
* @version 2.0.0
* @since 1.0.0
* @see InventoryItemValidationHelper
* @see InventoryItemAuditHelper
*/
@Service
@RequiredArgsConstructor
public class InventoryItemServiceImpl implements InventoryItemService {
private final InventoryItemRepository repository;
private final InventoryItemValidationHelper validationHelper;
private final InventoryItemAuditHelper auditHelper;
/**
* {@inheritDoc}
*
* <p><strong>Performance Warning</strong>: Loads ALL items. Use pagination for large datasets.
*
* @return list of all inventory items as DTOs
*/
@Override
public List<InventoryItemDTO> getAll() {
return repository.findAll().stream().map(InventoryItemMapper::toDTO).toList();
}
/**
* {@inheritDoc}
*
* @param id the unique identifier of the inventory item
* @return Optional containing the item DTO if found, empty otherwise
*/
@Override
public Optional<InventoryItemDTO> getById(String id) {
return repository.findById(id).map(InventoryItemMapper::toDTO);
}
/**
* {@inheritDoc}
*
* @param name the search term for item name (partial match supported)
* @param pageable pagination and sorting parameters
* @return paginated results sorted by price, empty page if no matches
*/
@Override
public Page<InventoryItemDTO> findByNameSortedByPrice(String name, Pageable pageable) {
Page<InventoryItem> page = repository.findByNameSortedByPrice(name, pageable);
return page == null ? Page.empty() : page.map(InventoryItemMapper::toDTO);
}
/**
* {@inheritDoc}
*
* @return total count of inventory items in the database
*/
@Override
@Transactional(readOnly = true)
public long countItems() {
return repository.count();
}
/**
* Creates a new inventory item with validation and audit trail initialization.
*
* @param dto the inventory item data transfer object
* @return the saved inventory item as DTO with server-generated fields
* @throws IllegalArgumentException if validation fails
*/
@Override
@Transactional
public InventoryItemDTO save(InventoryItemDTO dto) {
// Validate DTO for creation
validationHelper.validateForCreation(dto);
// Convert DTO to entity
InventoryItem entity = InventoryItemMapper.toEntity(dto);
// Populate server-side fields
validationHelper.populateServerFields(entity);
// Persist entity
InventoryItem saved = repository.save(entity);
// Log initial stock history
auditHelper.logInitialStock(saved);
return InventoryItemMapper.toDTO(saved);
}
/**
* Updates an existing inventory item with validation, security checks, and audit trail.
*
* @param id the unique identifier of the item to update
* @param dto the updated inventory item data
* @return Optional containing updated item DTO
* @throws IllegalArgumentException if validation fails
*/
@Override
@Transactional
public Optional<InventoryItemDTO> update(String id, InventoryItemDTO dto) {
// Validate for update and get existing item
InventoryItem existing = validationHelper.validateForUpdate(id, dto);
// Check uniqueness if name or price changed
validationHelper.validateUniquenessOnUpdate(id, existing, dto);
// Calculate quantity delta
int quantityDiff = dto.getQuantity() - existing.getQuantity();
// Update entity fields
existing.setName(dto.getName());
existing.setQuantity(dto.getQuantity());
existing.setSupplierId(dto.getSupplierId());
if (dto.getMinimumQuantity() > 0) {
existing.setMinimumQuantity(dto.getMinimumQuantity());
}
boolean priceChanged = !existing.getPrice().equals(dto.getPrice());
if (priceChanged) {
assertPriceValid(dto.getPrice());
existing.setPrice(dto.getPrice());
}
// Persist changes
InventoryItem updated = repository.save(existing);
// Log quantity change if delta is non-zero
auditHelper.logQuantityChange(updated, quantityDiff);
return Optional.of(InventoryItemMapper.toDTO(updated));
}
/**
* Deletes an inventory item after logging complete stock removal to audit trail.
*
* @param id the unique identifier of the item to delete
* @param reason the business reason for deletion
* @throws IllegalArgumentException if reason is invalid or item not found
*/
@Override
@Transactional
public void delete(String id, StockChangeReason reason) {
// Validate deletion reason
if (reason != StockChangeReason.SCRAPPED &&
reason != StockChangeReason.DESTROYED &&
reason != StockChangeReason.DAMAGED &&
reason != StockChangeReason.EXPIRED &&
reason != StockChangeReason.LOST &&
reason != StockChangeReason.RETURNED_TO_SUPPLIER) {
throw new IllegalArgumentException("Invalid reason for deletion");
}
// Validate item exists and quantity is zero before deletion
validationHelper.validateForDeletion(id);
InventoryItem item = validationHelper.validateExists(id);
// Log full stock removal
auditHelper.logFullRemoval(item, reason);
// Perform hard delete
repository.deleteById(id);
}
/**
* Adjusts inventory quantity by a delta (positive for stock-in, negative for stock-out).
*
* @param id the unique identifier of the item to adjust
* @param delta the quantity change
* @param reason the business reason for the adjustment
* @return the updated inventory item as DTO
* @throws IllegalArgumentException if item not found or final quantity would be negative
*/
@Override
@Transactional
public InventoryItemDTO adjustQuantity(String id, int delta, StockChangeReason reason) {
// Verify item exists
InventoryItem item = validationHelper.validateExists(id);
// Calculate new quantity
int newQty = item.getQuantity() + delta;
// Validate non-negative quantity
assertFinalQuantityNonNegative(newQty);
// Update item quantity
item.setQuantity(newQty);
// Persist to database
InventoryItem saved = repository.save(item);
// Log stock history
auditHelper.logQuantityAdjustment(saved, delta, reason);
return InventoryItemMapper.toDTO(saved);
}
/**
* Updates the unit price of an inventory item and logs a PRICE_CHANGE history entry.
*
* @param id the unique identifier of the item
* @param newPrice the new unit price (must be > 0)
* @return the updated inventory item as DTO
* @throws IllegalArgumentException if newPrice ≤ 0 or item not found
*/
@Override
@Transactional
public InventoryItemDTO updatePrice(String id, BigDecimal newPrice) {
// Validate price is positive
assertPriceValid(newPrice);
// Verify item exists
InventoryItem item = validationHelper.validateExists(id);
// Update item price
item.setPrice(newPrice);
// Persist to database
InventoryItem saved = repository.save(item);
// Log price change history
auditHelper.logPriceChange(id, newPrice);
return InventoryItemMapper.toDTO(saved);
}
/**
* Renames an inventory item (changes the item name).
* Validates that the new name is not a duplicate for the same supplier.
*
* @param id the unique identifier of the item
* @param newName the new item name (must not be empty)
* @return the updated inventory item as DTO
* @throws IllegalArgumentException if name is empty or already exists for this supplier
* @throws IllegalArgumentException if item not found
*/
@Override
@Transactional
public InventoryItemDTO renameItem(String id, String newName) {
// Validate new name is not empty
if (newName == null || newName.trim().isEmpty()) {
throw new IllegalArgumentException("Item name cannot be empty");
}
// Verify item exists and get existing data
InventoryItem existing = validationHelper.validateExists(id);
// Check if new name already exists for the same supplier (case-insensitive)
List<InventoryItem> duplicates = repository.findByNameIgnoreCase(newName.trim());
for (InventoryItem dup : duplicates) {
// If we find an item with the same name and supplier (but different id), it's a conflict
if (!dup.getId().equals(id) &&
dup.getSupplierId().equals(existing.getSupplierId())) {
throw new IllegalArgumentException("An item with this name already exists for this supplier");
}
}
// Update item name
existing.setName(newName.trim());
// Persist to database
InventoryItem saved = repository.save(existing);
return InventoryItemMapper.toDTO(saved);
}
}