⬅️ Back to Enums Hub

StockChangeReason Enum

Overview

The StockChangeReason enum provides enterprise-level categorization for all inventory movement tracking. It ensures consistent audit compliance, enables financial reconciliation, and supports operational analytics across the system. Every stock change must be classified with one of these standardized reasons.

Location: src/main/java/com/smartsupplypro/inventory/enums/StockChangeReason.java

Package: com.smartsupplypro.inventory.enums

Purpose: Type-safe stock movement classification for audit trails, financial reporting, and compliance


Enum Values

All Reasons (12 total)

public enum StockChangeReason {
    INITIAL_STOCK,          // Initial inventory entry
    MANUAL_UPDATE,          // Administrative adjustment
    PRICE_CHANGE,           // Price modification only
    SOLD,                   // Customer purchase
    SCRAPPED,               // Quality control removal
    DESTROYED,              // Catastrophic loss
    DAMAGED,                // Temporary quality hold
    EXPIRED,                // Expiration date breach
    LOST,                   // Unaccounted shrinkage
    RETURNED_TO_SUPPLIER,   // Vendor return
    RETURNED_BY_CUSTOMER;   // Customer return
}

Detailed Reason Descriptions

INITIAL_STOCK

Category: Initial Operations

Description: Initial inventory entry during item creation. Establishes the baseline for audit trail.

Business Impact: - Critical for audit trail establishment - First transaction in stock history - Baseline for variance analysis

Approval Required: No (auto-generated on item creation)

Affects Quantity: Yes

Compliance Documentation: No

Example:

Item created: Widget A, Quantity: 100
Reason: INITIAL_STOCK
Effect: Inventory increases to 100 units

Audit Severity: HIGH


MANUAL_UPDATE

Category: Administrative

Description: Manual administrative adjustment for inventory discrepancies discovered during physical counts or system corrections.

Business Impact: - Requires manager approval - Used for physical count reconciliation - Records discrepancies

Approval Required: Yes (Manager)

Affects Quantity: Yes

Compliance Documentation: No

Example:

Physical count found: 95 units (system: 100)
Adjustment: -5 units
Reason: MANUAL_UPDATE
Effect: Inventory corrected to 95 units

Audit Severity: MEDIUM


PRICE_CHANGE

Category: Administrative

Description: Price adjustment without quantity impact. Used for cost updates, supplier price changes, or financial corrections.

Business Impact: - Financial reporting classification - Affects cost basis and COGS - No quantity change

Approval Required: No (Financial team reviews)

Affects Quantity: No (only affects financial valuation)

Compliance Documentation: No

Example:

Supplier price update: $10 → $12 per unit
Quantity: 100 units (unchanged)
Reason: PRICE_CHANGE
Effect: Total value increases from $1,000 to $1,200

Audit Severity: LOW


SOLD

Category: Customer Transaction

Description: Customer purchase transaction. Revenue recognition and COGS calculation trigger.

Business Impact: - Revenue recognition event - COGS calculation trigger - Customer satisfaction metric - Sales tracking

Approval Required: No (auto-confirmed on order)

Affects Quantity: Yes (decreases)

Compliance Documentation: No

Example:

Order #ORD-001: Customer purchased 50 units
Price per unit: $12
Reason: SOLD
Effect: Inventory decreases to 50 units, Revenue = $600

Audit Severity: HIGH


SCRAPPED

Category: Quality Control

Description: Quality control removal for damaged, defective, or non-conforming items permanently removed from inventory.

Business Impact: - Loss prevention tracking - Quality metrics - Waste accounting - Potential supplier quality reviews

Approval Required: No (QC decision)

Affects Quantity: Yes (decreases)

Compliance Documentation: No

Example:

Quality inspection identified: 5 defective units
Action: Permanent removal
Reason: SCRAPPED
Effect: Inventory decreases by 5 units

Audit Severity: MEDIUM


DESTROYED

Category: Catastrophic Loss

Description: Catastrophic loss requiring insurance claim documentation. Represents major unrecoverable loss events.

Business Impact: - Insurance claim trigger - Asset write-off trigger - Loss investigation requirement - Potential legal/regulatory implications

Approval Required: Yes (Manager approval mandatory)

Affects Quantity: Yes (decreases significantly)

Compliance Documentation: Yes (Insurance claim required)

Example:

Warehouse fire destroyed inventory
Estimated loss: $50,000
Reason: DESTROYED
Effect: Total inventory write-off, Insurance claim filed

Audit Severity: CRITICAL


DAMAGED

Category: Quality Control

Description: Temporary quality hold for items pending repair, assessment, or disposition decision. Not a permanent removal.

Business Impact: - Operational impact tracking - Temporary inventory exclusion - Potential recovery option - Quality hold period monitoring

Approval Required: No (Quality team decision)

Affects Quantity: Yes (temporarily excluded from available inventory)

Compliance Documentation: No

Example:

Inspection found: 10 units with shipping damage
Action: Hold for assessment
Reason: DAMAGED
Effect: Inventory held, pending repair evaluation

Audit Severity: LOW


EXPIRED

Category: Regulatory Compliance

Description: Expiration date breach removal. Regulatory compliance requirement for perishable or time-sensitive goods.

Business Impact: - Regulatory compliance - Waste management - Quality assurance - Potential liability reduction

Approval Required: No (Auto-detected on date check)

Affects Quantity: Yes (decreases)

Compliance Documentation: Yes (Disposal records required)

Example:

Expiration date breach detected: 20 units expired 11/15/2025
Current date: 11/19/2025
Reason: EXPIRED
Effect: Inventory removed permanently, Disposal documented

Audit Severity: CRITICAL


LOST

Category: Security Incident

Description: Inventory shrinkage for unaccounted losses. Represents items that cannot be located in warehouse or system.

Business Impact: - Security review trigger - Shrinkage analysis - Warehouse process review - Potential theft investigation

Approval Required: Yes (Manager approval required)

Affects Quantity: Yes (decreases)

Compliance Documentation: Yes (Shrinkage report required)

Example:

Cycle count variance: 15 units missing
Cannot locate in warehouse
Reason: LOST
Effect: Inventory decreased, Shrinkage investigation initiated

Audit Severity: CRITICAL


RETURNED_TO_SUPPLIER

Category: Supplier Transaction

Description: Vendor return for defective, incorrect, or surplus merchandise. Supplier performance tracking trigger.

Business Impact: - Supplier performance tracking - Return process management - Inventory recovery - Cost recovery attempt

Approval Required: No (Supplier coordination)

Affects Quantity: Yes (decreases)

Compliance Documentation: No (RMA process applies)

Example:

Return merchandise authorization (RMA): RMA-5432
Reason: Defective merchandise from batch
Items returned: 8 units
Reason: RETURNED_TO_SUPPLIER
Effect: Inventory decreases, Cost recovery pending

Audit Severity: MEDIUM


RETURNED_BY_CUSTOMER

Category: Customer Transaction

Description: Customer return processing. Customer satisfaction and refund management trigger.

Business Impact: - Customer satisfaction - Refund management - Return process tracking - Potential restocking - Customer feedback

Approval Required: No (Customer service)

Affects Quantity: Yes (increases, if restockable)

Compliance Documentation: No

Example:

Return request: Customer found items defective
Order: ORD-001, Quantity: 5 units
Reason: RETURNED_BY_CUSTOMER
Effect: Inventory increased by 5 units, Refund processed

Audit Severity: LOW


Reason Categories

By Business Domain

Initial Operations: - INITIAL_STOCK

Administrative: - MANUAL_UPDATE - PRICE_CHANGE

Customer Transactions: - SOLD - RETURNED_BY_CUSTOMER

Quality Control: - DAMAGED - SCRAPPED

Security/Compliance: - DESTROYED - EXPIRED - LOST

Supplier Relations: - RETURNED_TO_SUPPLIER

By Quantity Impact

Affects Quantity (11):

INITIAL_STOCK, MANUAL_UPDATE, SOLD, SCRAPPED, DESTROYED,
DAMAGED, EXPIRED, LOST, RETURNED_TO_SUPPLIER, RETURNED_BY_CUSTOMER

Does NOT Affect Quantity (1):

PRICE_CHANGE

Enum Methods

Instance Methods

requiresManagerApproval()

Returns true if the reason requires manager-level approval before recording.

public boolean requiresManagerApproval()

Returns: true for: MANUAL_UPDATE, DESTROYED, LOST

Usage:

StockChangeReason reason = StockChangeReason.DESTROYED;
if (reason.requiresManagerApproval()) {
    // Require manager signature before processing
    requireManagerApproval(reason);
}

affectsQuantity()

Checks if this reason represents a quantity change.

public boolean affectsQuantity()

Returns: false only for PRICE_CHANGE, true for all others

Usage:

if (reason.affectsQuantity()) {
    inventory.adjustQuantity(change);
}

isLossReason()

Determines if this reason represents a financial loss.

public boolean isLossReason()

Returns: true for: SCRAPPED, DESTROYED, EXPIRED, LOST

Usage:

if (reason.isLossReason()) {
    financialReport.recordLoss(amount);
}

requiresComplianceDocumentation()

Checks if compliance documentation is required.

public boolean requiresComplianceDocumentation()

Returns: true for: EXPIRED, DESTROYED, LOST

Usage:

if (reason.requiresComplianceDocumentation()) {
    complianceSystem.createDocument(reason);
}

getAuditSeverity()

Returns the audit severity level for this reason.

public AuditSeverity getAuditSeverity()

Returns: AuditSeverity enum value

Mapping: - CRITICAL: DESTROYED, LOST - HIGH: INITIAL_STOCK, SOLD - MEDIUM: MANUAL_UPDATE, SCRAPPED, EXPIRED - LOW: PRICE_CHANGE, DAMAGED, RETURNED_BY_CUSTOMER, RETURNED_TO_SUPPLIER

Usage:

AuditSeverity severity = reason.getAuditSeverity();
logger.log(severity.getLogLevel(), "Stock change: " + reason);

Static Classification Methods

getLossReasons()

Returns all reasons representing inventory losses.

public static Set<StockChangeReason> getLossReasons()

Returns:

{SCRAPPED, DESTROYED, EXPIRED, LOST}

Usage:

Set<StockChangeReason> losses = StockChangeReason.getLossReasons();
BigDecimal totalLoss = stockHistory.stream()
    .filter(h -> losses.contains(h.getReason()))
    .map(StockHistory::getAmount)
    .reduce(BigDecimal.ZERO, BigDecimal::add);

getCustomerReasons()

Returns all reasons representing customer transactions.

public static Set<StockChangeReason> getCustomerReasons()

Returns:

{SOLD, RETURNED_BY_CUSTOMER}

Usage:

Set<StockChangeReason> customerTransactions = StockChangeReason.getCustomerReasons();
if (customerTransactions.contains(reason)) {
    customerAnalytics.recordTransaction(reason);
}

getSupplierReasons()

Returns all reasons representing supplier transactions.

public static Set<StockChangeReason> getSupplierReasons()

Returns:

{RETURNED_TO_SUPPLIER}

Usage:

if (StockChangeReason.getSupplierReasons().contains(reason)) {
    supplierPerformance.recordReturn(reason);
}

getSecuritySensitiveReasons()

Returns all reasons requiring security investigation.

public static Set<StockChangeReason> getSecuritySensitiveReasons()

Returns:

{LOST, DESTROYED}

Usage:

if (StockChangeReason.getSecuritySensitiveReasons().contains(reason)) {
    securityAlert.notifyWarehouse(reason);
}

parseReason(String)

Safely parses a string to StockChangeReason with detailed error handling.

public static StockChangeReason parseReason(String reasonString)

Features: - Null/empty check with descriptive error - Case-insensitive parsing - Clear error messages listing valid options

Parameters: - reasonString - String to parse (may be null)

Returns: Corresponding enum value

Throws: IllegalArgumentException with helpful error message

Examples:

// Valid parses
StockChangeReason r1 = StockChangeReason.parseReason("SOLD");           // ✅
StockChangeReason r2 = StockChangeReason.parseReason("sold");           // ✅ (case-insensitive)
StockChangeReason r3 = StockChangeReason.parseReason("  DAMAGED  ");    // ✅ (trimmed)

// Invalid parses
StockChangeReason r4 = StockChangeReason.parseReason("INVALID");        // ❌ Throws exception
StockChangeReason r5 = StockChangeReason.parseReason(null);             // ❌ Throws exception
StockChangeReason r6 = StockChangeReason.parseReason("");               // ❌ Throws exception

Error Message Example:

Invalid stock change reason 'INVALID'. Valid options: 
[INITIAL_STOCK, MANUAL_UPDATE, PRICE_CHANGE, SOLD, SCRAPPED, 
 DESTROYED, DAMAGED, EXPIRED, LOST, RETURNED_TO_SUPPLIER, RETURNED_BY_CUSTOMER]

Nested Enum: AuditSeverity

Classifies the severity level of stock change for audit and compliance purposes.

public enum AuditSeverity {
    LOW,        // Routine operations
    MEDIUM,     // Administrative adjustments
    HIGH,       // Revenue-impacting transactions
    CRITICAL    // Loss events requiring investigation
}

Severity Levels

Level Examples Action Required
LOW PRICE_CHANGE, DAMAGED, customer returns Monitor, routine logging
MEDIUM MANUAL_UPDATE, SCRAPPED, EXPIRED Review, document reason
HIGH INITIAL_STOCK, SOLD Track closely, audit trail
CRITICAL DESTROYED, LOST Immediate escalation, investigation

Database Schema

Storage

The enum is persisted as a STRING in the stock_history table:

ALTER TABLE stock_history ADD COLUMN reason VARCHAR(30);

-- Recommended index for frequent filtering
CREATE INDEX idx_stock_history_reason ON stock_history(reason);

-- Values: 'INITIAL_STOCK', 'SOLD', 'DAMAGED', etc.

Sample Data

-- Initial stock entry
INSERT INTO stock_history (id, item_id, change, reason, created_by, created_at)
VALUES ('SH-001', 'ITEM-001', 100, 'INITIAL_STOCK', 'system', NOW());

-- Customer sale
INSERT INTO stock_history (id, item_id, change, reason, created_by, created_at)
VALUES ('SH-002', 'ITEM-001', -50, 'SOLD', 'order@system', NOW());

-- Loss event
INSERT INTO stock_history (id, item_id, change, reason, created_by, created_at)
VALUES ('SH-003', 'ITEM-001', -5, 'LOST', 'warehouse-mgr', NOW());

DTO Serialization

StockHistoryDTO

@Data
public class StockHistoryDTO {
    private String id;
    private String itemId;
    private Integer change;
    private String reason;              // Serialized as string
    private String createdBy;
    private LocalDateTime timestamp;
}

API Examples

Request:

POST /api/stock-history
{
  "itemId": "ITEM-001",
  "change": -50,
  "reason": "SOLD"
}

Response (200 OK):

{
  "id": "SH-002",
  "itemId": "ITEM-001",
  "change": -50,
  "reason": "SOLD",
  "createdBy": "order@system",
  "timestamp": "2025-11-19T14:30:00Z"
}

Mapper Integration

public class StockHistoryMapper {
    
    public StockHistoryDTO toDTO(StockHistory entity) {
        return StockHistoryDTO.builder()
            .id(entity.getId())
            .itemId(entity.getItemId())
            .change(entity.getChange())
            .reason(entity.getReason().name())              // enum → string
            .createdBy(entity.getCreatedBy())
            .timestamp(entity.getCreatedAt())
            .build();
    }
    
    public StockHistory toEntity(StockHistoryDTO dto) {
        return StockHistory.builder()
            .itemId(dto.getItemId())
            .change(dto.getChange())
            .reason(StockChangeReason.parseReason(dto.getReason()))  // string → enum
            .createdBy(getCurrentUser())
            .createdAt(LocalDateTime.now())
            .build();
    }
}

Validation

Spring Validation

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = ValidStockChangeReasonValidator.class)
public @interface ValidStockChangeReason {
    String message() default "Invalid stock change reason";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class ValidStockChangeReasonValidator 
    implements ConstraintValidator<ValidStockChangeReason, String> {
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        try {
            StockChangeReason.parseReason(value);
            return true;
        } catch (IllegalArgumentException e) {
            return false;
        }
    }
}

DTO Validation

@Data
public class StockHistoryDTO {
    @NotNull(message = "Item ID is required")
    private String itemId;
    
    @NotNull(message = "Change quantity is required")
    private Integer change;
    
    @NotNull(message = "Stock change reason is required")
    @ValidStockChangeReason(message = "Invalid stock change reason")
    private String reason;
}

Testing

Unit Tests

@Test
void testStockChangeReasonMethods() {
    // Test requiresManagerApproval
    assertTrue(StockChangeReason.DESTROYED.requiresManagerApproval());
    assertFalse(StockChangeReason.SOLD.requiresManagerApproval());
    
    // Test affectsQuantity
    assertTrue(StockChangeReason.SOLD.affectsQuantity());
    assertFalse(StockChangeReason.PRICE_CHANGE.affectsQuantity());
    
    // Test isLossReason
    assertTrue(StockChangeReason.DESTROYED.isLossReason());
    assertFalse(StockChangeReason.SOLD.isLossReason());
    
    // Test requiresComplianceDocumentation
    assertTrue(StockChangeReason.EXPIRED.requiresComplianceDocumentation());
    assertFalse(StockChangeReason.DAMAGED.requiresComplianceDocumentation());
}

@Test
void testEnumClassification() {
    Set<StockChangeReason> losses = StockChangeReason.getLossReasons();
    assertEquals(4, losses.size());
    assertTrue(losses.contains(StockChangeReason.DESTROYED));
    assertTrue(losses.contains(StockChangeReason.LOST));
    assertFalse(losses.contains(StockChangeReason.SOLD));
}

@Test
void testReasonParsing() {
    // Valid
    assertEquals(StockChangeReason.SOLD, StockChangeReason.parseReason("SOLD"));
    assertEquals(StockChangeReason.DAMAGED, StockChangeReason.parseReason("  DAMAGED  "));
    
    // Invalid
    assertThrows(IllegalArgumentException.class,
        () -> StockChangeReason.parseReason("INVALID"));
    assertThrows(IllegalArgumentException.class,
        () -> StockChangeReason.parseReason(null));
}

@Test
void testAuditSeverity() {
    assertEquals(AuditSeverity.CRITICAL, StockChangeReason.DESTROYED.getAuditSeverity());
    assertEquals(AuditSeverity.HIGH, StockChangeReason.SOLD.getAuditSeverity());
    assertEquals(AuditSeverity.LOW, StockChangeReason.PRICE_CHANGE.getAuditSeverity());
}

Integration Tests

@SpringBootTest
@Transactional
class StockChangeReasonIT {
    
    @Autowired
    private StockHistoryRepository repository;
    
    @Test
    void testPersistenceWithEnum() {
        StockHistory history = StockHistory.builder()
            .itemId("ITEM-001")
            .change(-50)
            .reason(StockChangeReason.SOLD)
            .createdBy("order@system")
            .createdAt(LocalDateTime.now())
            .build();
        
        repository.save(history);
        
        StockHistory retrieved = repository.findById(history.getId()).get();
        assertEquals(StockChangeReason.SOLD, retrieved.getReason());
        assertEquals(-50, retrieved.getChange());
    }
    
    @Test
    void testReasonQuery() {
        repository.save(StockHistory.builder()
            .reason(StockChangeReason.SOLD)
            .itemId("ITEM-001")
            .change(-50)
            .createdAt(LocalDateTime.now())
            .build());
        
        repository.save(StockHistory.builder()
            .reason(StockChangeReason.DAMAGED)
            .itemId("ITEM-001")
            .change(0)
            .createdAt(LocalDateTime.now())
            .build());
        
        List<StockHistory> losses = repository
            .findByReasonIn(StockChangeReason.getLossReasons());
        assertEquals(0, losses.size());  // SOLD and DAMAGED are not losses
    }
}

Best Practices

DO: - Always use enum values in code (not string literals) - Use parseReason() for external input validation - Call appropriate classification methods before business logic - Document which reasons trigger approvals or compliance steps - Use EnumSet for efficient reason classification - Test enum method logic thoroughly

DON’T: - Hardcode reason strings in code - Forget to validate reason strings from API inputs - Ignore requiresManagerApproval() checks - Skip compliance documentation for sensitive reasons - Use ordinals for persistence - Add reasons without updating documentation


Migration Path

Adding a New Reason

Step 1: Update Enum

public enum StockChangeReason {
    INITIAL_STOCK,
    MANUAL_UPDATE,
    PRICE_CHANGE,
    SOLD,
    // ... existing reasons ...
    CUSTOMER_DONATION,  // New reason
}

Step 2: Update Methods

public boolean requiresManagerApproval() {
    return switch (this) {
        case MANUAL_UPDATE, DESTROYED, LOST, CUSTOMER_DONATION -> true;
        // ...
    };
}

// Add to appropriate classification methods
public static Set<StockChangeReason> getCustomerReasons() {
    return EnumSet.of(SOLD, RETURNED_BY_CUSTOMER, CUSTOMER_DONATION);
}

Step 3: Update Database (No migration needed) - VARCHAR(30) accommodates all reason strings - No data migration required

Step 4: Update Documentation - Add section in this document - Update related DTOs - Update validation rules if needed


Architecture: - Enums Hub - Overview of all enums - Data Models & Entities - Entity definitions - DTOs & Data Transfer Objects - Serialization patterns

Code Examples: - StockHistory Entity - StockHistoryMapper - StockHistoryValidator


⬅️ Back to Enums Hub