academia/docs/entregables/02-diseno/modelo-dominio/DI-002-modelo-dominio.md

272 lines
8.9 KiB
Markdown

# 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 │
└──────────────┘ └──────────────┘ └──────────────┘
```