# DI-008: Estrategia de Manejo de Errores

**Proyecto:** Sistema de Registro de Estudiantes
**Fecha:** 2026-01-07

---

## 1. Clasificación de Errores

| Tipo | Origen | Manejo | HTTP Code (equiv) |
|------|--------|--------|-------------------|
| **Validación** | FluentValidation | Payload.errors | 400 |
| **Dominio** | Domain Exceptions | Payload.errors | 422 |
| **Not Found** | Repository | Payload.errors | 404 |
| **Conflicto** | Concurrencia | Payload.errors | 409 |
| **Sistema** | Excepciones no manejadas | Error GraphQL | 500 |

---

## 2. Excepciones de Dominio

```csharp
// Base
public abstract class DomainException : Exception
{
    public string Code { get; }
    protected DomainException(string code, string message) : base(message)
        => Code = code;
}

// Específicas
public class MaxEnrollmentsExceededException : DomainException
{
    public MaxEnrollmentsExceededException()
        : base("MAX_ENROLLMENTS", "Máximo 3 materias permitidas") { }
}

public class SameProfessorConstraintException : DomainException
{
    public SameProfessorConstraintException(string professorName)
        : base("SAME_PROFESSOR", $"Ya tienes una materia con {professorName}") { }
}

public class DuplicateEmailException : DomainException
{
    public DuplicateEmailException()
        : base("DUPLICATE_EMAIL", "Este email ya está registrado") { }
}

public class StudentNotFoundException : DomainException
{
    public StudentNotFoundException(int id)
        : base("NOT_FOUND", $"Estudiante {id} no encontrado") { }
}
```

---

## 3. Patrón Result

```csharp
public class Result
{
    public bool IsSuccess { get; }
    public IEnumerable<string> Errors { get; }

    protected Result(bool success, IEnumerable<string>? errors = null)
    {
        IsSuccess = success;
        Errors = errors ?? Array.Empty<string>();
    }

    public static Result Success() => new(true);
    public static Result Failure(params string[] errors) => new(false, errors);
}

public class Result<T> : Result
{
    public T? Value { get; }

    private Result(T value) : base(true) => Value = value;
    private Result(IEnumerable<string> errors) : base(false, errors) { }

    public static Result<T> Success(T value) => new(value);
    public static new Result<T> Failure(params string[] errors) => new(errors);
}
```

---

## 4. Handler con Manejo de Errores

```csharp
public class EnrollStudentHandler
{
    public async Task<EnrollmentPayload> Handle(EnrollInput input)
    {
        try
        {
            var student = await _studentRepo.GetByIdWithEnrollmentsAsync(input.StudentId);
            if (student is null)
                return new EnrollmentPayload(null, ["Estudiante no encontrado"]);

            var subject = await _subjectRepo.GetByIdAsync(input.SubjectId);
            if (subject is null)
                return new EnrollmentPayload(null, ["Materia no encontrada"]);

            // Validación de dominio
            student.Enroll(subject, _enrollmentPolicy);

            await _unitOfWork.SaveChangesAsync();

            var dto = student.Enrollments.Last().Adapt<EnrollmentDto>();
            return new EnrollmentPayload(dto, null);
        }
        catch (DomainException ex)
        {
            return new EnrollmentPayload(null, [ex.Message]);
        }
    }
}
```

---

## 5. Error Filter GraphQL (HotChocolate)

```csharp
public class ErrorFilter : IErrorFilter
{
    public IError OnError(IError error)
    {
        return error.Exception switch
        {
            DomainException ex => error
                .WithMessage(ex.Message)
                .WithCode(ex.Code),

            ValidationException ex => error
                .WithMessage("Errores de validación")
                .WithCode("VALIDATION_ERROR")
                .SetExtension("errors", ex.Errors.Select(e => e.ErrorMessage)),

            DbUpdateConcurrencyException => error
                .WithMessage("Los datos fueron modificados por otro usuario")
                .WithCode("CONCURRENCY_ERROR"),

            _ => error
                .WithMessage("Error interno del servidor")
                .WithCode("INTERNAL_ERROR")
        };
    }
}
```

---

## 6. Manejo en Frontend

### Service

```typescript
@Injectable({ providedIn: 'root' })
export class ErrorHandlerService {
  private snackBar = inject(MatSnackBar);

  handleGraphQLErrors(errors: string[] | undefined): void {
    if (errors?.length) {
      this.snackBar.open(errors[0], 'Cerrar', {
        duration: 5000,
        panelClass: 'error-snackbar'
      });
    }
  }

  handleApolloError(error: ApolloError): void {
    const message = error.graphQLErrors[0]?.message ?? 'Error de conexión';
    this.snackBar.open(message, 'Cerrar', { duration: 5000 });
  }
}
```

### Componente

```typescript
enrollStudent(subjectId: number) {
  this.enrollmentService.enroll({
    studentId: this.studentId(),
    subjectId
  }).subscribe({
    next: ({ data }) => {
      if (data?.enrollStudent.errors?.length) {
        this.errorHandler.handleGraphQLErrors(data.enrollStudent.errors);
      } else {
        this.snackBar.open('Inscripción exitosa', 'OK');
      }
    },
    error: (err) => this.errorHandler.handleApolloError(err)
  });
}
```

---

## 7. Códigos de Error Estándar

| Código | Mensaje | Acción UI |
|--------|---------|-----------|
| `VALIDATION_ERROR` | Errores de validación | Mostrar en campos |
| `MAX_ENROLLMENTS` | Máximo 3 materias | Toast + deshabilitar |
| `SAME_PROFESSOR` | Ya tienes materia con X | Toast + deshabilitar |
| `DUPLICATE_EMAIL` | Email ya registrado | Error en campo |
| `NOT_FOUND` | Recurso no encontrado | Redirect + toast |
| `CONCURRENCY_ERROR` | Datos modificados | Refetch + toast |
| `INTERNAL_ERROR` | Error del servidor | Toast genérico |

---

## 8. Logging de Errores

```csharp
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
    private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;

    public async Task<TResponse> Handle(TRequest request, ...)
    {
        try
        {
            return await next();
        }
        catch (Exception ex) when (ex is not DomainException)
        {
            _logger.LogError(ex, "Error procesando {Request}", typeof(TRequest).Name);
            throw;
        }
    }
}
```

**Regla:** NO loguear datos sensibles (email, contraseñas).
