# Entity Validators

## Overview

Entity Validators are responsible for validating domain entities at the business logic level, ensuring data integrity and enforcing business rules that go beyond simple form validation. They operate on entity models and provide comprehensive validation for complex business scenarios.

## Purpose

Entity Validators serve several key functions:
- **Business Rule Enforcement** - Apply domain-specific validation rules
- **Data Integrity** - Ensure entity data consistency
- **Cross-Entity Validation** - Validate relationships between entities
- **State Validation** - Verify entity state transitions are valid
- **Constraint Enforcement** - Apply database and domain constraints

## Structure

Each entity validator typically follows this pattern:

```go
type PlantEntityValidator struct {
    Validator     *validator.Validator
    PlantFetcher  *fetchers.PlantFetcher
    GardenFetcher *fetchers.GardenFetcher
}

func (this *PlantEntityValidator) ValidateForCreate(ctx context.Context, entity *mdl.Plant) error {
    bag := this.Validator.MakeEntityBag()

    this.validateRequiredFields(entity, bag)
    this.validateBusinessRules(ctx, entity, bag)
    this.validateConstraints(ctx, entity, bag)

    if !this.Validator.IsValid(bag) {
        return this.Validator.NewEntityValidationError(bag)
    }

    return nil
}
```

## Validation Types

### Required Field Validation

```go
func (this *PlantEntityValidator) validateRequiredFields(entity *mdl.Plant, bag *validator.EntityBag) {
    if strings.TrimSpace(entity.Name) == "" {
        this.Validator.AddEntityError(bag, "name", "Plant name is required")
    }

    if strings.TrimSpace(entity.Species) == "" {
        this.Validator.AddEntityError(bag, "species", "Plant species is required")
    }

    if entity.GardenId == "" {
        this.Validator.AddEntityError(bag, "garden_id", "Garden assignment is required")
    }
}
```

### Business Rule Validation

```go
func (this *PlantEntityValidator) validateBusinessRules(ctx context.Context, entity *mdl.Plant, bag *validator.EntityBag) {
    // Perennial plants must be of minimum size
    if entity.Perennial && entity.Size < 20 {
        this.Validator.AddEntityError(bag, "size", "Perennial plants must be at least 20cm")
    }

    // Harvest dates must be logical
    if !entity.PlantedAt.IsZero() && !entity.HarvestedAt.IsZero() {
        if entity.HarvestedAt.Before(entity.PlantedAt) {
            this.Validator.AddEntityError(bag, "harvested_at", "Harvest date cannot be before planting date")
        }
    }

    // Edible plants require additional data
    if entity.Edible && entity.DaysToHarvest == 0 {
        this.Validator.AddEntityError(bag, "days_to_harvest", "Edible plants must specify days to harvest")
    }
}
```

### Relationship Validation

```go
func (this *PlantEntityValidator) validateConstraints(ctx context.Context, entity *mdl.Plant, bag *validator.EntityBag) {
    // Validate garden exists and is active
    if entity.GardenId != "" {
        garden, exists, err := this.GardenFetcher.FindOneById(ctx, entity.GardenId, nil)
        if err != nil {
            this.Validator.AddEntityError(bag, "garden_id", "Unable to validate garden")
            return
        }

        if !exists {
            this.Validator.AddEntityError(bag, "garden_id", "Garden does not exist")
            return
        }

        if !garden.Active {
            this.Validator.AddEntityError(bag, "garden_id", "Cannot add plants to inactive garden")
        }

        // Check garden capacity
        if err := this.validateGardenCapacity(ctx, garden, entity, bag); err != nil {
            this.Validator.AddEntityError(bag, "garden_id", "Garden capacity validation failed")
        }
    }
}
```

### Uniqueness Validation

```go
func (this *PlantEntityValidator) validateUniqueness(ctx context.Context, entity *mdl.Plant, bag *validator.EntityBag) {
    // Check for unique name within garden
    mod := this.PlantFetcher.Mod()
    mod.ExactStringValueFilter("garden_id", entity.GardenId)
    mod.ExactStringValueFilter("name", entity.Name)

    if entity.Id != "" {
        mod.UnequalStringValueFilter("id", entity.Id) // Exclude self for updates
    }

    existing, err := this.PlantFetcher.FindSet(ctx, mod)
    if err != nil {
        this.Validator.AddEntityError(bag, "name", "Unable to validate plant name uniqueness")
        return
    }

    if len(existing) > 0 {
        this.Validator.AddEntityError(bag, "name", "Plant name must be unique within garden")
    }
}
```

## State-Specific Validation

### Create Validation

```go
func (this *PlantEntityValidator) ValidateForCreate(ctx context.Context, entity *mdl.Plant) error {
    bag := this.Validator.MakeEntityBag()

    this.validateRequiredFields(entity, bag)
    this.validateBusinessRules(ctx, entity, bag)
    this.validateConstraints(ctx, entity, bag)
    this.validateUniqueness(ctx, entity, bag)

    if !this.Validator.IsValid(bag) {
        return this.Validator.NewEntityValidationError(bag)
    }

    return nil
}
```

### Update Validation

```go
func (this *PlantEntityValidator) ValidateForUpdate(ctx context.Context, entity *mdl.Plant, original *mdl.Plant) error {
    bag := this.Validator.MakeEntityBag()

    this.validateRequiredFields(entity, bag)
    this.validateBusinessRules(ctx, entity, bag)
    this.validateConstraints(ctx, entity, bag)
    this.validateUniqueness(ctx, entity, bag)
    this.validateStateTransitions(entity, original, bag)

    if !this.Validator.IsValid(bag) {
        return this.Validator.NewEntityValidationError(bag)
    }

    return nil
}
```

### State Transition Validation

```go
func (this *PlantEntityValidator) validateStateTransitions(entity *mdl.Plant, original *mdl.Plant, bag *validator.EntityBag) {
    // Cannot unharvest a plant
    if !original.HarvestedAt.IsZero() && entity.HarvestedAt.IsZero() {
        this.Validator.AddEntityError(bag, "harvested_at", "Cannot unharvest a plant")
    }

    // Cannot change garden if plant has been harvested
    if !original.HarvestedAt.IsZero() && entity.GardenId != original.GardenId {
        this.Validator.AddEntityError(bag, "garden_id", "Cannot move harvested plants")
    }

    // Cannot reduce size of living plants
    if original.HarvestedAt.IsZero() && entity.Size < original.Size {
        this.Validator.AddEntityError(bag, "size", "Cannot reduce size of living plants")
    }
}
```

### Delete Validation

```go
func (this *PlantEntityValidator) ValidateForDelete(ctx context.Context, entity *mdl.Plant) error {
    bag := this.Validator.MakeEntityBag()

    // Check if plant has active tasks
    if hasActiveTasks, err := this.hasActiveTasks(ctx, entity); err != nil {
        this.Validator.AddEntityError(bag, "id", "Unable to validate plant dependencies")
    } else if hasActiveTasks {
        this.Validator.AddEntityError(bag, "id", "Cannot delete plant with active tasks")
    }

    // Check if plant is referenced in seed programs
    if isInSeedPrograms, err := this.isInSeedPrograms(ctx, entity); err != nil {
        this.Validator.AddEntityError(bag, "id", "Unable to validate seed program references")
    } else if isInSeedPrograms {
        this.Validator.AddEntityError(bag, "id", "Cannot delete plant referenced in seed programs")
    }

    if !this.Validator.IsValid(bag) {
        return this.Validator.NewEntityValidationError(bag)
    }

    return nil
}
```

## Complex Business Rules

### Seasonal Validation

```go
func (this *PlantEntityValidator) validateSeasonalRules(entity *mdl.Plant, bag *validator.EntityBag) {
    if !entity.PlantedAt.IsZero() {
        month := entity.PlantedAt.Month()

        // Check if planting season is appropriate for this species
        if entity.Species == "Tomato" {
            if month < time.April || month > time.June {
                this.Validator.AddEntityError(bag, "planted_at", "Tomatoes should be planted between April and June")
            }
        }

        if entity.Species == "Lettuce" {
            if month < time.March || month > time.September {
                this.Validator.AddEntityError(bag, "planted_at", "Lettuce should be planted between March and September")
            }
        }
    }
}
```

### Capacity Validation

```go
func (this *PlantEntityValidator) validateGardenCapacity(ctx context.Context, garden *mdl.Garden, plant *mdl.Plant, bag *validator.EntityBag) error {
    // Get current plants in garden
    mod := this.PlantFetcher.Mod()
    mod.ExactStringValueFilter("garden_id", garden.Id)
    mod.AbsentValueFilter("harvested_at") // Only count living plants

    if plant.Id != "" {
        mod.UnequalStringValueFilter("id", plant.Id) // Exclude self for updates
    }

    currentPlants, err := this.PlantFetcher.FindSet(ctx, mod)
    if err != nil {
        return err
    }

    // Calculate space usage
    currentSpace := 0
    for _, p := range currentPlants {
        currentSpace += p.Size
    }

    totalSpace := currentSpace + plant.Size
    if totalSpace > garden.MaxCapacity {
        this.Validator.AddEntityError(bag, "size", fmt.Sprintf("Garden capacity exceeded. Available: %d, Required: %d", garden.MaxCapacity-currentSpace, plant.Size))
    }

    return nil
}
```

## Integration with Maestros

Entity validators are typically used in Maestro operations:

```go
func (this *CreatePlantMae) Execute(ctx context.Context, input *maes.CreatePlantMaeInput) (*maes.CreatePlantMaeOutput, error) {
    // Transform form to entity
    plant, err := this.PlantMolder.MoldFromCreateForm(input.Form)
    if err != nil {
        return nil, fmt.Errorf("failed to mold plant: %w", err)
    }

    // Validate entity
    if err := this.PlantEntityValidator.ValidateForCreate(ctx, plant); err != nil {
        return nil, fmt.Errorf("plant validation failed: %w", err)
    }

    // Create plant
    createdPlant, err := this.PlantHandler.Create(ctx, plant, nil)
    if err != nil {
        return nil, fmt.Errorf("failed to create plant: %w", err)
    }

    return &maes.CreatePlantMaeOutput{Plant: createdPlant}, nil
}
```

## Error Handling

### Entity Validation Errors

```go
type EntityValidationError struct {
    Errors map[string][]string
}

func (e *EntityValidationError) Error() string {
    var messages []string
    for field, errors := range e.Errors {
        messages = append(messages, fmt.Sprintf("%s: %s", field, strings.Join(errors, ", ")))
    }
    return fmt.Sprintf("Entity validation failed: %s", strings.Join(messages, "; "))
}
```

### Graceful Error Handling

```go
func (this *PlantEntityValidator) validateWithFallback(ctx context.Context, entity *mdl.Plant, bag *validator.EntityBag) {
    defer func() {
        if r := recover(); r != nil {
            this.Validator.AddEntityError(bag, "validation", "Internal validation error occurred")
        }
    }()

    // Perform validation...
}
```

## Testing Entity Validators

```go
func TestPlantEntityValidator_ValidateForCreate_Success(t *testing.T) {
    validator := setupPlantEntityValidator()

    plant := &mdl.Plant{
        Name:      "Tomato",
        Species:   "Solanum lycopersicum",
        GardenId:  "garden123",
        Size:      50,
        Perennial: false,
    }

    err := validator.ValidateForCreate(context.Background(), plant)

    assert.NoError(t, err)
}

func TestPlantEntityValidator_ValidateForCreate_ValidationErrors(t *testing.T) {
    validator := setupPlantEntityValidator()

    plant := &mdl.Plant{
        Name:      "", // Invalid - required
        Size:      -5, // Invalid - negative
        Perennial: true,
    }

    err := validator.ValidateForCreate(context.Background(), plant)

    assert.Error(t, err)

    var validationErr *EntityValidationError
    assert.True(t, errors.As(err, &validationErr))
    assert.Contains(t, validationErr.Errors, "name")
    assert.Contains(t, validationErr.Errors, "size")
}
```

## Best Practices

### Performance
- Validate cheapest rules first
- Cache expensive lookups when possible
- Avoid N+1 queries in relationship validation

### Maintainability
- Keep validation rules close to business logic
- Use descriptive error messages
- Extract common validation patterns

### Reliability
- Handle database errors gracefully
- Provide fallbacks for external dependencies
- Log validation failures for monitoring

### Consistency
- Use consistent error message formats
- Apply the same validation rules across all operations
- Maintain validation rule documentation

Entity Validators ensure business rule compliance and data integrity at the domain level, providing a robust validation layer that maintains application consistency and reliability.