# DI-002: Modelo de Dominio

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

---

## 1. Diagrama de Entidades

```
┌─────────────────┐       ┌─────────────────────────┐
│    PROFESSOR    │       │        STUDENT          │
├─────────────────┤       ├─────────────────────────┤
│ Id: int (PK)    │       │ Id: int (PK)            │
│ Name: string    │       │ Name: string            │
└────────┬────────┘       │ Email: Email            │
         │                │ ActivationCodeHash?     │  ← Nuevo
         │ 1:2            │ ActivationExpiresAt?    │  ← Nuevo
         ▼                │ IsActivated (computed)  │  ← Nuevo
┌─────────────────┐       │ RowVersion              │
│     SUBJECT     │       └────────┬────────────────┘
├─────────────────┤                │
│ Id: int (PK)    │                │ 0..3
│ Name: string    │                ▼
│ Credits: 3      │       ┌─────────────────┐
│ ProfessorId: FK │◄──────│   ENROLLMENT    │
└─────────────────┘  1:N  ├─────────────────┤
                          │ Id: int (PK)    │
┌─────────────────┐       │ StudentId: FK   │
│      USER       │       │ SubjectId: FK   │
├─────────────────┤       │ EnrolledAt      │
│ Id: int (PK)    │       └─────────────────┘
│ Username        │
│ PasswordHash    │
│ RecoveryCodeHash│
│ Role (Admin/    │
│       Student)  │
│ StudentId?: FK  │───────► 0..1 Student
│ CreatedAt       │
│ LastLoginAt?    │
└─────────────────┘
```

---

## 2. Entidades

### Student (Aggregate Root)

```csharp
public class Student
{
    public const int MaxEnrollments = 3;

    public int Id { get; private set; }
    public string Name { get; private set; }
    public Email Email { get; private set; }

    // Campos de Activación (nuevo flujo)
    public string? ActivationCodeHash { get; private set; }
    public DateTime? ActivationExpiresAt { get; private set; }
    public bool IsActivated => ActivationCodeHash == null;

    private readonly List<Enrollment> _enrollments = new();
    public IReadOnlyCollection<Enrollment> Enrollments => _enrollments;

    public int TotalCredits => _enrollments.Count * 3;

    public void Enroll(Subject subject, IEnrollmentPolicy policy)
    {
        policy.Validate(this, subject);
        _enrollments.Add(new Enrollment(this, subject));
    }

    public void Unenroll(int subjectId)
    {
        var enrollment = _enrollments.FirstOrDefault(e => e.SubjectId == subjectId);
        if (enrollment != null) _enrollments.Remove(enrollment);
    }

    // Métodos de activación
    public void SetActivationCode(string codeHash, TimeSpan expiresIn)
    {
        ActivationCodeHash = codeHash;
        ActivationExpiresAt = DateTime.UtcNow.Add(expiresIn);
    }

    public void ClearActivationCode()
    {
        ActivationCodeHash = null;
        ActivationExpiresAt = null;
    }

    public bool IsActivationExpired() =>
        ActivationExpiresAt.HasValue && DateTime.UtcNow > ActivationExpiresAt.Value;
}
```

### User (Autenticación)

```csharp
public class User
{
    public int Id { get; private set; }
    public string Username { get; private set; }      // Almacenado en minúsculas
    public string PasswordHash { get; private set; }  // PBKDF2-SHA256
    public string RecoveryCodeHash { get; private set; }
    public string Role { get; private set; }          // "Admin" | "Student"
    public int? StudentId { get; private set; }       // FK opcional a Student
    public DateTime CreatedAt { get; private set; }
    public DateTime? LastLoginAt { get; private set; }

    public Student? Student { get; private set; }     // Navegación
}
```

### Subject

```csharp
public class Subject
{
    public int Id { get; private set; }
    public string Name { get; private set; }
    public int Credits { get; } = 3;  // Constante: RN-002
    public int ProfessorId { get; private set; }
    public Professor Professor { get; private set; }
}
```

### Professor

```csharp
public class Professor
{
    public int Id { get; private set; }
    public string Name { get; private set; }

    private readonly List<Subject> _subjects = new();
    public IReadOnlyCollection<Subject> Subjects => _subjects;
}
```

### Enrollment

```csharp
public class Enrollment
{
    public int Id { get; private set; }
    public int StudentId { get; private set; }
    public int SubjectId { get; private set; }
    public DateTime EnrolledAt { get; private set; }

    public Student Student { get; private set; }
    public Subject Subject { get; private set; }
}
```

---

## 3. Value Objects

### Email

```csharp
public record Email
{
    public string Value { get; }

    private Email(string value) => Value = value;

    public static Result<Email> Create(string email)
    {
        if (string.IsNullOrWhiteSpace(email))
            return Result.Failure<Email>("Email requerido");

        if (!Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$"))
            return Result.Failure<Email>("Formato de email inválido");

        return Result.Success(new Email(email.ToLowerInvariant()));
    }
}
```

---

## 4. Domain Service

### EnrollmentDomainService (Implementa RN-003 y RN-005)

```csharp
public class EnrollmentDomainService : IEnrollmentPolicy
{
    public void Validate(Student student, Subject newSubject)
    {
        // RN-003: Máximo 3 materias
        if (student.Enrollments.Count >= 3)
            throw new MaxEnrollmentsExceededException(student.Id);

        // RN-005: No repetir profesor
        var professorIds = student.Enrollments
            .Select(e => e.Subject.ProfessorId)
            .ToHashSet();

        if (professorIds.Contains(newSubject.ProfessorId))
            throw new SameProfessorConstraintException(
                student.Id,
                newSubject.ProfessorId);
    }
}
```

---

## 5. Excepciones de Dominio

```csharp
public class MaxEnrollmentsExceededException : DomainException
{
    public MaxEnrollmentsExceededException(int studentId)
        : base($"Estudiante {studentId} ya tiene 3 materias inscritas") { }
}

public class SameProfessorConstraintException : DomainException
{
    public SameProfessorConstraintException(int studentId, int professorId)
        : base($"Estudiante {studentId} ya tiene materia con profesor {professorId}") { }
}
```

---

## 6. Invariantes del Dominio

| Invariante | Entidad | Validación |
|------------|---------|------------|
| Email único | Student | DB Constraint + Validator |
| Email válido | Student | Value Object |
| Max 3 inscripciones | Student | Domain Service |
| No repetir profesor | Student | Domain Service |
| 3 créditos/materia | Subject | Constante |
| 2 materias/profesor | Professor | Seed Data |
| Username único | User | DB Constraint |
| Código activación expira | Student | ActivationExpiresAt |
| Student ↔ User (1:0..1) | User | StudentId nullable |

---

## 7. Flujo de Activación

```
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│    ADMIN     │    │   STUDENT    │    │     USER     │
│  (creates)   │───►│ (pending)    │    │  (not yet)   │
└──────────────┘    └──────┬───────┘    └──────────────┘
                           │
                    SetActivationCode()
                           │
                           ▼
                    ┌──────────────┐
                    │   STUDENT    │
                    │ (has code)   │
                    └──────┬───────┘
                           │
                    Activate(username, password)
                           │
                           ▼
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│    ADMIN     │    │   STUDENT    │◄───│     USER     │
│  (creates)   │    │ (activated)  │    │ StudentId=X  │
└──────────────┘    └──────────────┘    └──────────────┘
```
