StockHistoryService.java
package com.smartsupplypro.inventory.service;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import com.smartsupplypro.inventory.dto.StockHistoryDTO;
import com.smartsupplypro.inventory.enums.StockChangeReason;
import com.smartsupplypro.inventory.mapper.StockHistoryMapper;
import com.smartsupplypro.inventory.model.StockHistory;
import com.smartsupplypro.inventory.repository.InventoryItemRepository;
import com.smartsupplypro.inventory.repository.StockHistoryRepository;
import com.smartsupplypro.inventory.validation.StockHistoryValidator;
import lombok.RequiredArgsConstructor;
/**
* Service for immutable stock movement event logging and audit trail management.
*
* <p><strong>Characteristics</strong>:
* <ul>
* <li><strong>Event Sourcing</strong>: Immutable history records for all inventory changes</li>
* <li><strong>Denormalization</strong>: Stores supplier ID for efficient analytics queries</li>
* <li><strong>Price Snapshots</strong>: Captures unit price at transaction time for WAC</li>
* <li><strong>Audit Compliance</strong>: Tracks user, timestamp, reason for all changes</li>
* <li><strong>Read-Only Operations</strong>: Query methods with filtering and pagination</li>
* </ul>
*
* <p><strong>Architecture Documentation</strong>:
* For event sourcing patterns, denormalization strategy, compliance features, and integration details, see:
* <a href="../../../../../docs/architecture/services/stock-history-service.md">Stock History Service Architecture</a>
*
* @see StockHistory
* @see StockHistoryValidator
* @see StockHistoryMapper
*/
@Service
@RequiredArgsConstructor
public class StockHistoryService {
private final StockHistoryRepository repository;
private final InventoryItemRepository itemRepository;
/**
* Resolves supplier ID for denormalization in stock history records.
* @param itemId inventory item ID
* @return supplier ID or null if item/supplier not found
*/
private String resolveSupplierId(String itemId) {
// Enterprise Comment: Denormalization for Analytics Performance
// Supplier ID stored directly on history record to avoid joins
// Enables efficient supplier-centric analytics queries with index: (SUPPLIER_ID, CREATED_AT)
return itemRepository.findById(itemId)
.map(item -> item.getSupplierId())
.orElse(null); // keep null-safe; index still works for present values
}
/**
* Retrieves all stock history entries.
* @return list of stock history DTOs
*/
public List<StockHistoryDTO> getAll() {
return repository.findAll().stream()
.map(StockHistoryMapper::toDTO)
.collect(Collectors.toList());
}
/**
* Retrieves stock history for specific inventory item (newest first).
* @param itemId inventory item ID
* @return list of stock history DTOs ordered by timestamp descending
*/
public List<StockHistoryDTO> getByItemId(String itemId) {
// Prefer ordered repo method (added in our repository suggestions)
var list = repository.findByItemIdOrderByTimestampDesc(itemId);
return list.stream().map(StockHistoryMapper::toDTO).toList();
}
/**
* Retrieves stock history for specific change reason (newest first).
* @param reason stock change reason filter
* @return list of stock history DTOs ordered by timestamp descending
*/
public List<StockHistoryDTO> getByReason(StockChangeReason reason) {
var list = repository.findByReasonOrderByTimestampDesc(reason);
return list.stream().map(StockHistoryMapper::toDTO).toList();
}
/**
* Retrieves paginated stock history filtered by date range, item name, and supplier.
* @param startDate start date (inclusive)
* @param endDate end date (inclusive)
* @param itemName item name filter (partial match)
* @param supplierId supplier ID filter
* @param pageable pagination parameters
* @return page of stock history DTOs
*/
public Page<StockHistoryDTO> findFiltered(LocalDateTime startDate,
LocalDateTime endDate,
String itemName,
String supplierId,
Pageable pageable) {
return repository.findFiltered(startDate, endDate, itemName, supplierId, pageable)
.map(StockHistoryMapper::toDTO);
}
/**
* Logs stock change without price snapshot (backwards compatibility).
* @param itemId inventory item ID
* @param change quantity change (positive or negative)
* @param reason business reason for change
* @param createdBy user who initiated change
* @throws IllegalArgumentException if input validation fails
*/
public void logStockChange(String itemId, int change, StockChangeReason reason, String createdBy) {
// Delegate to the price-aware overload with a null snapshot
logStockChange(itemId, change, reason, createdBy, null);
}
/**
* Logs stock change with price snapshot for analytics (WAC calculation).
* @param itemId inventory item ID
* @param change quantity change (positive or negative)
* @param reason business reason for change
* @param createdBy user who initiated change
* @param priceAtChange unit price snapshot at time of change (nullable)
* @throws IllegalArgumentException if input validation fails
*/
public void logStockChange(String itemId,
int change,
StockChangeReason reason,
String createdBy,
BigDecimal priceAtChange) {
// Validate the enum value & basic constraints
StockHistoryValidator.validateEnum(reason);
// Enterprise Comment: DTO Construction for Validation
// Price snapshot enables WAC calculations and PRICE_CHANGE tracking
// Construct a DTO for validation (includes price snapshot when provided)
StockHistoryDTO dto = StockHistoryDTO.builder()
.itemId(itemId)
.change(change)
.reason(reason.name()) // If DTO already uses enum, set it directly instead of name()
.createdBy(createdBy)
.priceAtChange(priceAtChange)
.build();
// Business validation (e.g., zero allowed only for PRICE_CHANGE, etc.)
StockHistoryValidator.validate(dto);
// Enterprise Comment: Denormalization Pattern
// Resolve supplier ID once and store on history record for analytics
String supplierId = resolveSupplierId(itemId);
// Enterprise Comment: Immutable Event Creation
// Server-authoritative timestamp ensures consistency across distributed systems
// Persist entity with server-authoritative timestamp
StockHistory history = StockHistory.builder()
.id("sh-" + itemId + "-" + System.currentTimeMillis())
.itemId(itemId)
.supplierId(supplierId)
.change(change)
.reason(reason) // entity uses enum directly
.createdBy(createdBy)
.timestamp(LocalDateTime.now())
.priceAtChange(priceAtChange)
.build();
repository.save(history);
}
/**
* Persists validated stock history DTO with server timestamp.
* @param dto validated stock history DTO
* @throws IllegalArgumentException if validation fails
*/
public void save(StockHistoryDTO dto) {
// Validate DTO with domain rules
StockHistoryValidator.validate(dto);
String supplierId = resolveSupplierId(dto.getItemId());
// Map & persist
StockHistory history = StockHistory.builder()
.id("sh-" + dto.getItemId() + "-" + System.currentTimeMillis())
.itemId(dto.getItemId())
.supplierId(supplierId)
.change(dto.getChange())
.reason(dto.getReason() != null
? StockChangeReason.valueOf(dto.getReason())
: null)
.createdBy(dto.getCreatedBy())
.timestamp(LocalDateTime.now())
.priceAtChange(dto.getPriceAtChange())
.build();
repository.save(history);
}
/**
* Records item deletion in stock history (legacy convention: -1 quantity).
* @param itemId inventory item ID
* @param reason deletion reason
* @param createdBy user who initiated deletion
*/
public void delete(String itemId, StockChangeReason reason, String createdBy) {
logStockChange(itemId, -1, reason, createdBy);
}
}