# 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 _enrollments = new(); public IReadOnlyCollection 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 _subjects = new(); public IReadOnlyCollection 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 Create(string email) { if (string.IsNullOrWhiteSpace(email)) return Result.Failure("Email requerido"); if (!Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$")) return Result.Failure("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 │ └──────────────┘ └──────────────┘ └──────────────┘ ```