docs: add project documentation and env template
Environment: - .env.example template with all configuration variables - Database, API, GraphQL, and frontend settings Documentation: - Architecture Decision Records (ADR-001 to ADR-004) - Deployment guide with Docker and K8s instructions - OWASP security checklist - Code review checklist - Activity plan and deliverables Architecture diagrams (PlantUML): - Use cases, domain model, sequence diagrams - Component, ER, state, and deployment diagrams - C4 context diagram Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
|
@ -0,0 +1,42 @@
|
|||
# =============================================================================
|
||||
# Student Enrollment System - Environment Variables
|
||||
# =============================================================================
|
||||
# Copiar este archivo a .env y ajustar los valores
|
||||
# NUNCA commitear el archivo .env con credenciales reales
|
||||
# =============================================================================
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Database (SQL Server)
|
||||
# -----------------------------------------------------------------------------
|
||||
DB_HOST=localhost
|
||||
DB_PORT=1433
|
||||
DB_NAME=StudentEnrollment
|
||||
DB_USER=sa
|
||||
DB_PASSWORD=Your_Str0ng_P@ssword!
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# API (.NET)
|
||||
# -----------------------------------------------------------------------------
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
API_PORT=5000
|
||||
CORS_ORIGINS=http://localhost:4200
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# GraphQL
|
||||
# -----------------------------------------------------------------------------
|
||||
GRAPHQL_MAX_DEPTH=5
|
||||
GRAPHQL_MAX_COMPLEXITY=100
|
||||
GRAPHQL_INTROSPECTION=true
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Logging (Serilog)
|
||||
# -----------------------------------------------------------------------------
|
||||
LOG_LEVEL=Information
|
||||
LOG_PATH=logs/app-.log
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Frontend (Angular)
|
||||
# -----------------------------------------------------------------------------
|
||||
FRONTEND_PORT=4200
|
||||
GRAPHQL_URL=http://localhost:5000/graphql
|
||||
HEALTH_CHECK_URL=http://localhost:5000/health
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
# Code Review Checklist - Sistema Registro Estudiantes
|
||||
|
||||
## Estado: Validado
|
||||
|
||||
### Arquitectura (Clean Architecture)
|
||||
|
||||
| Criterio | Estado | Evidencia |
|
||||
|----------|--------|-----------|
|
||||
| Separación de capas | ✅ | Domain, Application, Adapters, Host |
|
||||
| Regla de dependencia | ✅ | Domain no depende de nada externo |
|
||||
| Ports & Adapters | ✅ | IStudentRepository, IEnrollmentRepository |
|
||||
| CQRS implementado | ✅ | Commands y Queries separados |
|
||||
|
||||
### Principios SOLID
|
||||
|
||||
| Principio | Estado | Evidencia |
|
||||
|-----------|--------|-----------|
|
||||
| **S**ingle Responsibility | ✅ | Cada clase tiene una responsabilidad |
|
||||
| **O**pen/Closed | ✅ | Extensible via interfaces |
|
||||
| **L**iskov Substitution | ✅ | Repositorios intercambiables |
|
||||
| **I**nterface Segregation | ✅ | Interfaces específicas por entidad |
|
||||
| **D**ependency Inversion | ✅ | Inyección de dependencias |
|
||||
|
||||
### Clean Code
|
||||
|
||||
| Criterio | Estado | Notas |
|
||||
|----------|--------|-------|
|
||||
| Naming conventions | ✅ | PascalCase clases, camelCase variables |
|
||||
| Métodos pequeños | ✅ | < 20 líneas promedio |
|
||||
| Sin código duplicado | ✅ | DRY aplicado |
|
||||
| Comentarios mínimos | ✅ | Código autodocumentado |
|
||||
| Archivos < 100 líneas | ✅ | Refactorizado donde necesario |
|
||||
|
||||
### Seguridad
|
||||
|
||||
| Criterio | Estado | Ubicación |
|
||||
|----------|--------|-----------|
|
||||
| Input validation | ✅ | FluentValidation + Regex |
|
||||
| SQL Injection prevention | ✅ | EF Core parametrizado |
|
||||
| XSS prevention | ✅ | Sanitización en validators |
|
||||
| Security headers | ✅ | Program.cs middleware |
|
||||
| Rate limiting | ✅ | 100 req/min |
|
||||
| Query complexity limits | ✅ | Depth 5, complexity 100 |
|
||||
|
||||
### Testing
|
||||
|
||||
| Tipo | Cantidad | Cobertura |
|
||||
|------|----------|-----------|
|
||||
| Domain Tests | 30 | Entidades, ValueObjects, Services |
|
||||
| Application Tests | 66 | Commands, Queries, Validators |
|
||||
| Integration Tests | 5 | GraphQL flujo completo |
|
||||
| Angular Unit Tests | 24 | Services, Pipes |
|
||||
| E2E Tests (Playwright) | 20 | Flujos principales |
|
||||
| **Total** | **145** | |
|
||||
|
||||
### Convenciones de Código
|
||||
|
||||
#### Backend (.NET)
|
||||
|
||||
- [x] Async/await en operaciones I/O
|
||||
- [x] Records para DTOs inmutables
|
||||
- [x] Nullable habilitado
|
||||
- [x] Global usings configurados
|
||||
- [x] FluentValidation para validaciones
|
||||
|
||||
#### Frontend (Angular)
|
||||
|
||||
- [x] Standalone components
|
||||
- [x] Signals para estado reactivo
|
||||
- [x] Lazy loading por feature
|
||||
- [x] OnPush change detection
|
||||
- [x] Apollo Client para GraphQL
|
||||
|
||||
### GraphQL
|
||||
|
||||
| Criterio | Estado |
|
||||
|----------|--------|
|
||||
| Types bien definidos | ✅ |
|
||||
| DataLoaders para N+1 | ✅ |
|
||||
| Error handling | ✅ |
|
||||
| Payloads con errors | ✅ |
|
||||
| Depth limiting | ✅ |
|
||||
|
||||
### Performance
|
||||
|
||||
| Optimización | Implementada |
|
||||
|--------------|--------------|
|
||||
| Response compression | ✅ Brotli + Gzip |
|
||||
| Output caching | ✅ 5 min para subjects/professors |
|
||||
| Apollo cache | ✅ cache-and-network |
|
||||
| Lazy loading | ✅ Por feature module |
|
||||
| Bundle optimization | ✅ < 800KB initial |
|
||||
|
||||
### Documentación
|
||||
|
||||
| Documento | Estado |
|
||||
|-----------|--------|
|
||||
| README.md | ✅ |
|
||||
| CLAUDE.md | ✅ |
|
||||
| OWASP_CHECKLIST.md | ✅ |
|
||||
| GraphQL Schema | ✅ (Banana Cake Pop) |
|
||||
|
||||
---
|
||||
|
||||
## Checklist de Revisión Manual
|
||||
|
||||
### Antes de Merge
|
||||
|
||||
- [ ] Todos los tests pasan
|
||||
- [ ] Build sin errores ni warnings
|
||||
- [ ] Código formateado
|
||||
- [ ] Sin TODOs pendientes críticos
|
||||
- [ ] Variables de entorno documentadas
|
||||
|
||||
### Seguridad
|
||||
|
||||
- [ ] Sin secrets hardcodeados
|
||||
- [ ] Sin console.log en producción
|
||||
- [ ] Validaciones en frontend Y backend
|
||||
- [ ] Error messages no exponen detalles internos
|
||||
|
||||
### UX
|
||||
|
||||
- [ ] Loading states implementados
|
||||
- [ ] Error messages claros
|
||||
- [ ] Responsive design funcional
|
||||
- [ ] Accesibilidad básica (a11y)
|
||||
|
|
@ -0,0 +1,419 @@
|
|||
# Manual de Despliegue
|
||||
|
||||
## Requisitos del Sistema
|
||||
|
||||
| Componente | Versión Mínima |
|
||||
|------------|----------------|
|
||||
| .NET SDK | 10.0 |
|
||||
| Node.js | 22.x |
|
||||
| SQL Server | 2022 |
|
||||
| Docker | 24.x |
|
||||
| Docker Compose | 2.x |
|
||||
|
||||
## Variables de Entorno
|
||||
|
||||
### Backend (.NET)
|
||||
|
||||
| Variable | Descripción | Ejemplo |
|
||||
|----------|-------------|---------|
|
||||
| `ConnectionStrings__DefaultConnection` | Connection string SQL Server | `Server=db;Database=StudentEnrollment;...` |
|
||||
| `ASPNETCORE_ENVIRONMENT` | Ambiente | `Production` |
|
||||
| `ASPNETCORE_URLS` | URLs de escucha | `http://+:5000` |
|
||||
|
||||
### Frontend (Angular)
|
||||
|
||||
| Variable | Descripción | Ejemplo |
|
||||
|----------|-------------|---------|
|
||||
| `API_URL` | URL del backend GraphQL | `https://api.example.com/graphql` |
|
||||
|
||||
## Despliegue con Docker
|
||||
|
||||
### 1. Estructura de Archivos
|
||||
|
||||
```
|
||||
deploy/
|
||||
└── docker/
|
||||
├── Dockerfile.api
|
||||
├── Dockerfile.frontend
|
||||
├── docker-compose.yml
|
||||
└── nginx.conf
|
||||
```
|
||||
|
||||
### 2. Dockerfile Backend
|
||||
|
||||
```dockerfile
|
||||
# deploy/docker/Dockerfile.api
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
COPY src/backend/ .
|
||||
RUN dotnet restore Host/Host.csproj
|
||||
RUN dotnet publish Host/Host.csproj -c Release -o /app
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0
|
||||
WORKDIR /app
|
||||
COPY --from=build /app .
|
||||
|
||||
# Non-root user
|
||||
RUN adduser --disabled-password --gecos '' appuser
|
||||
USER appuser
|
||||
|
||||
EXPOSE 5000
|
||||
HEALTHCHECK --interval=30s --timeout=3s \
|
||||
CMD curl -f http://localhost:5000/health || exit 1
|
||||
|
||||
ENTRYPOINT ["dotnet", "Host.dll"]
|
||||
```
|
||||
|
||||
### 3. Dockerfile Frontend
|
||||
|
||||
```dockerfile
|
||||
# deploy/docker/Dockerfile.frontend
|
||||
FROM node:22-alpine AS build
|
||||
WORKDIR /app
|
||||
|
||||
COPY src/frontend/package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY src/frontend/ .
|
||||
RUN npm run build -- --configuration production
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=build /app/dist/student-enrollment/browser /usr/share/nginx/html
|
||||
COPY deploy/docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
HEALTHCHECK --interval=30s --timeout=3s \
|
||||
CMD curl -f http://localhost/ || exit 1
|
||||
```
|
||||
|
||||
### 4. Nginx Configuration
|
||||
|
||||
```nginx
|
||||
# deploy/docker/nginx.conf
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript;
|
||||
|
||||
# SPA routing
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Proxy GraphQL
|
||||
location /graphql {
|
||||
proxy_pass http://api:5000/graphql;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Docker Compose
|
||||
|
||||
```yaml
|
||||
# deploy/docker/docker-compose.yml
|
||||
services:
|
||||
db:
|
||||
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||
environment:
|
||||
- ACCEPT_EULA=Y
|
||||
- SA_PASSWORD=${DB_PASSWORD}
|
||||
ports:
|
||||
- "1433:1433"
|
||||
volumes:
|
||||
- sqlserver-data:/var/opt/mssql
|
||||
healthcheck:
|
||||
test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $${SA_PASSWORD} -Q "SELECT 1" -C
|
||||
interval: 10s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/docker/Dockerfile.api
|
||||
environment:
|
||||
- ConnectionStrings__DefaultConnection=Server=db;Database=StudentEnrollment;User Id=sa;Password=${DB_PASSWORD};TrustServerCertificate=True
|
||||
- ASPNETCORE_ENVIRONMENT=Production
|
||||
ports:
|
||||
- "5000:5000"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ../..
|
||||
dockerfile: deploy/docker/Dockerfile.frontend
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- api
|
||||
|
||||
volumes:
|
||||
sqlserver-data:
|
||||
```
|
||||
|
||||
### 6. Ejecutar
|
||||
|
||||
```bash
|
||||
cd deploy/docker
|
||||
|
||||
# Crear archivo .env
|
||||
echo "DB_PASSWORD=Your_Str0ng_P@ssword!" > .env
|
||||
|
||||
# Build e iniciar
|
||||
docker-compose up -d --build
|
||||
|
||||
# Ver logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Detener
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
## Despliegue Manual
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
cd src/backend/Host
|
||||
|
||||
# Build producción
|
||||
dotnet publish -c Release -o ./publish
|
||||
|
||||
# Configurar connection string
|
||||
export ConnectionStrings__DefaultConnection="Server=..."
|
||||
|
||||
# Ejecutar migraciones
|
||||
dotnet ef database update --project ../Adapters/Driven/Persistence
|
||||
|
||||
# Iniciar
|
||||
cd publish
|
||||
dotnet Host.dll
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
cd src/frontend
|
||||
|
||||
# Build producción
|
||||
ng build --configuration production
|
||||
|
||||
# Los archivos quedan en dist/student-enrollment/browser/
|
||||
# Servir con cualquier servidor web (nginx, apache, etc.)
|
||||
```
|
||||
|
||||
## Checklist Pre-Producción
|
||||
|
||||
### Seguridad
|
||||
|
||||
- [ ] Connection strings en variables de entorno (no en código)
|
||||
- [ ] HTTPS habilitado
|
||||
- [ ] CORS configurado solo para dominios permitidos
|
||||
- [ ] Rate limiting activo
|
||||
- [ ] Security headers configurados
|
||||
- [ ] Logs sin datos sensibles
|
||||
|
||||
### Performance
|
||||
|
||||
- [ ] Response compression habilitado
|
||||
- [ ] Output caching configurado
|
||||
- [ ] Bundle Angular optimizado (< 200KB transferidos)
|
||||
- [ ] Índices de BD aplicados
|
||||
|
||||
### Monitoreo
|
||||
|
||||
- [ ] Health checks funcionando (`/health`)
|
||||
- [ ] Logs centralizados
|
||||
- [ ] Métricas de aplicación
|
||||
|
||||
### Base de Datos
|
||||
|
||||
- [ ] Backup configurado
|
||||
- [ ] Migraciones aplicadas
|
||||
- [ ] Datos seed cargados (5 profesores, 10 materias)
|
||||
|
||||
## URLs de Verificación
|
||||
|
||||
| Servicio | URL | Esperado |
|
||||
|----------|-----|----------|
|
||||
| API Health | `http://api:5000/health` | 200 OK |
|
||||
| GraphQL Playground | `http://api:5000/graphql` | Banana Cake Pop |
|
||||
| Frontend | `http://frontend:80` | App Angular |
|
||||
|
||||
## Rollback
|
||||
|
||||
```bash
|
||||
# Docker
|
||||
docker-compose down
|
||||
docker-compose up -d --no-build # Usa imágenes anteriores
|
||||
|
||||
# Manual
|
||||
# Restaurar versión anterior de DLLs/archivos
|
||||
# Rollback de migraciones si es necesario:
|
||||
dotnet ef database update <MigrationName>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Despliegue en Kubernetes (k3s)
|
||||
|
||||
### Estructura de Manifiestos
|
||||
|
||||
```
|
||||
deploy/k3s/
|
||||
├── namespace.yaml # Namespace dedicado
|
||||
├── secrets.yaml # Credenciales BD
|
||||
├── configmap.yaml # Configuración
|
||||
├── sqlserver.yaml # Base de datos
|
||||
├── api.yaml # Backend GraphQL
|
||||
├── frontend.yaml # Frontend Angular
|
||||
├── ingress.yaml # Traefik ingress
|
||||
├── ingress-tls.yaml # TLS con cert-manager
|
||||
├── hpa.yaml # Autoscaling
|
||||
├── networkpolicy.yaml # Seguridad de red
|
||||
├── kustomization.yaml # Kustomize config
|
||||
└── deploy.sh # Script de despliegue
|
||||
```
|
||||
|
||||
### Requisitos k3s
|
||||
|
||||
- k3s instalado y funcionando
|
||||
- kubectl configurado
|
||||
- Acceso al cluster
|
||||
- (Opcional) cert-manager para TLS
|
||||
|
||||
### Despliegue Rápido
|
||||
|
||||
```bash
|
||||
cd deploy/k3s
|
||||
|
||||
# Opción 1: Con script
|
||||
./deploy.sh all # Build + Deploy
|
||||
|
||||
# Opción 2: Con kustomize
|
||||
kubectl apply -k .
|
||||
|
||||
# Verificar estado
|
||||
kubectl get all -n student-enrollment
|
||||
```
|
||||
|
||||
### Comandos del Script
|
||||
|
||||
```bash
|
||||
./deploy.sh build # Construir imágenes Docker
|
||||
./deploy.sh deploy # Desplegar a k3s
|
||||
./deploy.sh status # Ver estado del cluster
|
||||
./deploy.sh logs api # Ver logs del API
|
||||
./deploy.sh forward # Port-forward para desarrollo
|
||||
./deploy.sh delete # Eliminar deployment
|
||||
```
|
||||
|
||||
### Configurar Secrets
|
||||
|
||||
```bash
|
||||
# Editar secrets antes de desplegar
|
||||
kubectl create secret generic student-secrets \
|
||||
--namespace=student-enrollment \
|
||||
--from-literal=db-password='TuPasswordSeguro123!' \
|
||||
--from-literal=db-connection-string='Server=sqlserver;Database=StudentEnrollment;User Id=sa;Password=TuPasswordSeguro123!;TrustServerCertificate=True' \
|
||||
--dry-run=client -o yaml > secrets.yaml
|
||||
```
|
||||
|
||||
### Acceso Local (Desarrollo)
|
||||
|
||||
```bash
|
||||
# Agregar entrada en /etc/hosts
|
||||
echo "127.0.0.1 students.local" | sudo tee -a /etc/hosts
|
||||
|
||||
# Port forward
|
||||
./deploy.sh forward
|
||||
|
||||
# Acceder en:
|
||||
# - Frontend: http://localhost:8080
|
||||
# - API: http://localhost:5000/graphql
|
||||
```
|
||||
|
||||
### Habilitar TLS (Producción)
|
||||
|
||||
```bash
|
||||
# 1. Instalar cert-manager
|
||||
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.14.0/cert-manager.yaml
|
||||
|
||||
# 2. Editar ingress-tls.yaml con tu dominio y email
|
||||
|
||||
# 3. Aplicar ingress con TLS
|
||||
kubectl apply -f ingress-tls.yaml -n student-enrollment
|
||||
```
|
||||
|
||||
### Scaling Manual
|
||||
|
||||
```bash
|
||||
# Escalar API
|
||||
kubectl scale deployment student-api -n student-enrollment --replicas=3
|
||||
|
||||
# Escalar Frontend
|
||||
kubectl scale deployment student-frontend -n student-enrollment --replicas=2
|
||||
```
|
||||
|
||||
### Monitoreo
|
||||
|
||||
```bash
|
||||
# Estado de pods
|
||||
kubectl get pods -n student-enrollment -w
|
||||
|
||||
# Logs en tiempo real
|
||||
kubectl logs -n student-enrollment -l app=student-api -f
|
||||
|
||||
# Eventos
|
||||
kubectl get events -n student-enrollment --sort-by='.lastTimestamp'
|
||||
|
||||
# Recursos
|
||||
kubectl top pods -n student-enrollment
|
||||
```
|
||||
|
||||
### Rollback en k3s
|
||||
|
||||
```bash
|
||||
# Ver historial de deployments
|
||||
kubectl rollout history deployment/student-api -n student-enrollment
|
||||
|
||||
# Rollback a versión anterior
|
||||
kubectl rollout undo deployment/student-api -n student-enrollment
|
||||
|
||||
# Rollback a revisión específica
|
||||
kubectl rollout undo deployment/student-api -n student-enrollment --to-revision=2
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
```bash
|
||||
# Pod no inicia
|
||||
kubectl describe pod <pod-name> -n student-enrollment
|
||||
|
||||
# Conectar a pod
|
||||
kubectl exec -it <pod-name> -n student-enrollment -- /bin/sh
|
||||
|
||||
# Verificar conectividad BD
|
||||
kubectl exec -it <api-pod> -n student-enrollment -- \
|
||||
curl -v telnet://sqlserver:1433
|
||||
|
||||
# Verificar ingress
|
||||
kubectl describe ingress student-ingress -n student-enrollment
|
||||
```
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
# Entregables - Prueba Técnica Senior .NET/Angular
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Empresa:** Inter Rapidísimo
|
||||
**Cargo:** Desarrollador Master .NET/Angular
|
||||
|
||||
---
|
||||
|
||||
## Resumen Ejecutivo
|
||||
|
||||
Sistema web completo para gestión de inscripciones de estudiantes con las siguientes características:
|
||||
|
||||
- **Backend:** .NET 10, GraphQL (HotChocolate), Clean Architecture
|
||||
- **Frontend:** Angular 21, Standalone Components, Signals
|
||||
- **Base de Datos:** SQL Server con EF Core
|
||||
- **Seguridad:** OWASP compliant, rate limiting, query complexity
|
||||
- **Testing:** 145 tests automatizados
|
||||
- **Despliegue:** Docker + Kubernetes (k3s)
|
||||
|
||||
---
|
||||
|
||||
## Estructura del Proyecto
|
||||
|
||||
```
|
||||
/
|
||||
├── src/
|
||||
│ ├── backend/ # .NET 10 API GraphQL
|
||||
│ │ ├── Domain/ # Entidades, Value Objects, Ports
|
||||
│ │ ├── Application/ # Commands, Queries, DTOs
|
||||
│ │ ├── Adapters/ # GraphQL API, Persistence
|
||||
│ │ └── Host/ # Entry point, DI
|
||||
│ └── frontend/ # Angular 21 SPA
|
||||
├── tests/ # Tests automatizados
|
||||
├── docs/ # Documentación
|
||||
└── deploy/ # Docker + k3s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Funcionalidades Implementadas
|
||||
|
||||
### Requisitos Funcionales
|
||||
|
||||
| # | Requisito | Estado |
|
||||
|---|-----------|--------|
|
||||
| 1 | CRUD completo de estudiantes | ✅ |
|
||||
| 2 | Inscripción en materias (max 3) | ✅ |
|
||||
| 3 | Visualización de compañeros de clase | ✅ |
|
||||
| 4 | Restricción de profesor único | ✅ |
|
||||
| 5 | 10 materias, 5 profesores | ✅ |
|
||||
| 6 | 3 créditos por materia | ✅ |
|
||||
| 7 | Validación de inscripciones | ✅ |
|
||||
| 8 | UI responsiva | ✅ |
|
||||
| 9 | Manejo de errores | ✅ |
|
||||
|
||||
### Reglas de Negocio
|
||||
|
||||
- ✅ Máximo 3 materias por estudiante (9 créditos)
|
||||
- ✅ No repetir profesor en inscripciones
|
||||
- ✅ Validación en Domain Layer (pura, testeable)
|
||||
- ✅ Mensajes de error descriptivos
|
||||
|
||||
---
|
||||
|
||||
## Arquitectura
|
||||
|
||||
### Clean Architecture
|
||||
|
||||
```
|
||||
Host → Adapters → Application → Domain
|
||||
```
|
||||
|
||||
- **Domain:** Entidades puras, sin dependencias
|
||||
- **Application:** Casos de uso con CQRS
|
||||
- **Adapters:** GraphQL API + EF Core
|
||||
- **Host:** Composición y DI
|
||||
|
||||
### Patrones Implementados
|
||||
|
||||
| Patrón | Uso |
|
||||
|--------|-----|
|
||||
| CQRS | Separación Commands/Queries |
|
||||
| Repository | Abstracción de persistencia |
|
||||
| Ports & Adapters | Inversión de dependencias |
|
||||
| DataLoader | Evitar N+1 en GraphQL |
|
||||
| Specification | Consultas reutilizables |
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Resumen de Tests
|
||||
|
||||
| Tipo | Cantidad | Cobertura |
|
||||
|------|----------|-----------|
|
||||
| Domain Tests | 30 | Entidades, Value Objects, Services |
|
||||
| Application Tests | 66 | Commands, Queries, Validators |
|
||||
| Integration Tests | 5 | GraphQL API completa |
|
||||
| Angular Unit Tests | 24 | Services, Pipes |
|
||||
| E2E Tests (Playwright) | 20 | Flujos de usuario |
|
||||
| **Total** | **145** | |
|
||||
|
||||
### Ejecutar Tests
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
dotnet test
|
||||
|
||||
# Frontend
|
||||
cd src/frontend
|
||||
ng test --watch=false
|
||||
|
||||
# E2E
|
||||
npx playwright test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Seguridad (OWASP)
|
||||
|
||||
| Control | Implementación |
|
||||
|---------|----------------|
|
||||
| Input Validation | FluentValidation + Regex |
|
||||
| SQL Injection | EF Core parametrizado |
|
||||
| XSS Prevention | Sanitización en validators |
|
||||
| Security Headers | CSP, HSTS, X-Frame-Options |
|
||||
| Rate Limiting | 100 req/min (fixed window) |
|
||||
| Query Complexity | Depth 5, Cost 100 |
|
||||
| Logging | Serilog (sin datos sensibles) |
|
||||
|
||||
---
|
||||
|
||||
## Documentación
|
||||
|
||||
| Documento | Ubicación |
|
||||
|-----------|-----------|
|
||||
| README principal | `/README.md` |
|
||||
| Arquitectura Backend | `/docs/entregables/02-diseno/arquitectura/` |
|
||||
| Modelo de Dominio | `/docs/entregables/02-diseno/modelo-dominio/` |
|
||||
| Diseño BD | `/docs/entregables/02-diseno/base-datos/` |
|
||||
| Esquema GraphQL | `/docs/entregables/02-diseno/esquema-graphql/` |
|
||||
| ADRs | `/docs/architecture/decisions/` |
|
||||
| OWASP Checklist | `/docs/OWASP_CHECKLIST.md` |
|
||||
| Manual Despliegue | `/docs/DEPLOYMENT.md` |
|
||||
|
||||
---
|
||||
|
||||
## Despliegue
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
cd deploy/docker
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
### Kubernetes (k3s)
|
||||
|
||||
```bash
|
||||
cd deploy/k3s
|
||||
./deploy.sh all
|
||||
```
|
||||
|
||||
### URLs
|
||||
|
||||
| Servicio | URL |
|
||||
|----------|-----|
|
||||
| Frontend | http://localhost:80 |
|
||||
| API GraphQL | http://localhost:5000/graphql |
|
||||
| Playground | http://localhost:5000/graphql |
|
||||
|
||||
---
|
||||
|
||||
## Competencias Demostradas
|
||||
|
||||
### Arquitectura y Diseño
|
||||
|
||||
- ✅ Clean Architecture con separación de capas
|
||||
- ✅ Principios SOLID aplicados
|
||||
- ✅ Domain-Driven Design (DDD lite)
|
||||
- ✅ CQRS para separación de concerns
|
||||
|
||||
### Backend (.NET)
|
||||
|
||||
- ✅ .NET 10 con C# 13
|
||||
- ✅ GraphQL con HotChocolate
|
||||
- ✅ EF Core con Fluent API
|
||||
- ✅ FluentValidation
|
||||
- ✅ DataLoaders para N+1
|
||||
|
||||
### Frontend (Angular)
|
||||
|
||||
- ✅ Angular 21 con Standalone Components
|
||||
- ✅ Signals para estado reactivo
|
||||
- ✅ Apollo Client para GraphQL
|
||||
- ✅ Angular Material UI
|
||||
- ✅ Lazy Loading
|
||||
|
||||
### DevOps
|
||||
|
||||
- ✅ Docker multi-stage builds
|
||||
- ✅ Kubernetes manifests
|
||||
- ✅ Health checks
|
||||
- ✅ Horizontal Pod Autoscaler
|
||||
|
||||
### Testing
|
||||
|
||||
- ✅ Unit tests con alta cobertura
|
||||
- ✅ Integration tests
|
||||
- ✅ E2E tests con Playwright
|
||||
- ✅ Mocking de dependencias
|
||||
|
||||
### Seguridad
|
||||
|
||||
- ✅ OWASP Top 10 compliance
|
||||
- ✅ Input validation
|
||||
- ✅ Rate limiting
|
||||
- ✅ Security headers
|
||||
|
||||
---
|
||||
|
||||
## Cómo Ejecutar
|
||||
|
||||
### Desarrollo Local
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd src/backend/Host
|
||||
dotnet run
|
||||
|
||||
# Frontend
|
||||
cd src/frontend
|
||||
npm install
|
||||
ng serve
|
||||
```
|
||||
|
||||
### Con Docker
|
||||
|
||||
```bash
|
||||
cd deploy/docker
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
# Todos los tests backend
|
||||
dotnet test
|
||||
|
||||
# Tests frontend
|
||||
cd src/frontend && ng test
|
||||
|
||||
# E2E
|
||||
cd src/frontend && npx playwright test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decisiones Técnicas
|
||||
|
||||
Ver ADRs completos en `/docs/architecture/decisions/`:
|
||||
|
||||
1. **ADR-001:** Clean Architecture - Testabilidad y mantenibilidad
|
||||
2. **ADR-002:** GraphQL vs REST - Flexibilidad en queries
|
||||
3. **ADR-003:** Signals vs RxJS - Simplicidad en estado local
|
||||
4. **ADR-004:** Validación 3 capas - Defensa en profundidad
|
||||
|
||||
---
|
||||
|
||||
*Desarrollado como prueba técnica para Inter Rapidísimo*
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
# OWASP Top 10 Security Checklist
|
||||
|
||||
## Estado: Validado
|
||||
|
||||
| # | Vulnerabilidad | Mitigación Implementada | Ubicación |
|
||||
|---|---------------|-------------------------|-----------|
|
||||
| A01 | **Broken Access Control** | No aplica (sin autenticación requerida) | N/A |
|
||||
| A02 | **Cryptographic Failures** | HTTPS forzado en producción (HSTS) | `Program.cs:127` |
|
||||
| A03 | **Injection** | FluentValidation + Regex sanitization, EF Core parameterized queries | `CreateStudentValidator.cs`, `EnrollStudentValidator.cs` |
|
||||
| A04 | **Insecure Design** | Clean Architecture, input validation en todas las capas | Arquitectura por capas |
|
||||
| A05 | **Security Misconfiguration** | Security headers (CSP, X-Frame-Options, etc.), Exception details disabled in prod | `Program.cs:106-130`, `GraphQLExtensions.cs:53` |
|
||||
| A06 | **Vulnerable Components** | Dependencias actualizadas (.NET 10, Angular 21) | `*.csproj`, `package.json` |
|
||||
| A07 | **Auth Failures** | No aplica (sin autenticación en este MVP) | N/A |
|
||||
| A08 | **Data Integrity Failures** | Input validation, FluentValidation, GraphQL type safety | Validators |
|
||||
| A09 | **Security Logging Failures** | Serilog structured logging, sensitive data filtering | `appsettings.json:38-45` |
|
||||
| A10 | **Server-Side Request Forgery** | No endpoints que acepten URLs externas | N/A |
|
||||
|
||||
## Medidas de Seguridad Implementadas
|
||||
|
||||
### Backend (.NET)
|
||||
|
||||
1. **Input Validation**
|
||||
- FluentValidation con regex patterns
|
||||
- Sanitización de HTML/scripts
|
||||
- Longitud máxima de campos
|
||||
- Validación de formato email
|
||||
|
||||
2. **Security Headers**
|
||||
- `Content-Security-Policy`
|
||||
- `X-Content-Type-Options: nosniff`
|
||||
- `X-Frame-Options: DENY`
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin`
|
||||
- `Permissions-Policy`
|
||||
- `Strict-Transport-Security` (producción)
|
||||
|
||||
3. **Rate Limiting**
|
||||
- 100 requests/minuto para queries GraphQL
|
||||
- 30 mutations/minuto
|
||||
- Queue limit para prevenir acumulación
|
||||
|
||||
4. **GraphQL Security**
|
||||
- Query depth limit: 5 niveles
|
||||
- Query complexity limit: 100
|
||||
- Execution timeout: 30 segundos
|
||||
- Pagination max: 50 items
|
||||
|
||||
5. **Logging Seguro**
|
||||
- Filtrado de datos sensibles (passwords, tokens)
|
||||
- Structured logging con Serilog
|
||||
- Rotación de logs (7 días)
|
||||
|
||||
### Frontend (Angular)
|
||||
|
||||
1. **XSS Prevention**
|
||||
- Angular sanitization por defecto
|
||||
- Content Security Policy
|
||||
|
||||
2. **CSRF Protection**
|
||||
- No cookies de sesión (stateless GraphQL)
|
||||
|
||||
3. **Secure Communication**
|
||||
- Solo HTTPS en producción
|
||||
- GraphQL sobre HTTPS
|
||||
|
||||
## Pruebas de Seguridad Recomendadas
|
||||
|
||||
```bash
|
||||
# Test security headers
|
||||
curl -I http://localhost:5000/graphql
|
||||
|
||||
# Test rate limiting (debe retornar 429 después de 100 requests)
|
||||
for i in {1..150}; do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:5000/graphql; done
|
||||
|
||||
# Test query depth (debe fallar con depth > 5)
|
||||
curl -X POST http://localhost:5000/graphql \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"{ students { enrollments { subject { professor { subjects { name } } } } } }"}'
|
||||
```
|
||||
|
|
@ -0,0 +1,245 @@
|
|||
# Plan de Actividades - Prueba Técnica Senior .NET/Angular
|
||||
|
||||
## Información del Proyecto
|
||||
- **Cargo:** Desarrollador Master .NET/Angular
|
||||
- **Empresa:** Inter Rapidísimo
|
||||
- **Proyecto:** Sistema de Registro de Estudiantes
|
||||
- **Stack:** .NET 10 + GraphQL (HotChocolate) + Angular 21 + SQL Server
|
||||
|
||||
---
|
||||
|
||||
## Procesos de Fábrica de Software
|
||||
|
||||
| Código | Proceso | Descripción |
|
||||
|--------|---------|-------------|
|
||||
| **AN** | Análisis | Levantamiento de requisitos, historias de usuario |
|
||||
| **DI** | Diseño | Arquitectura, modelos, prototipos UI/UX |
|
||||
| **DE** | Desarrollo | Codificación, implementación |
|
||||
| **QA** | Quality Assurance | Testing, revisión de código |
|
||||
| **DV** | DevOps | CI/CD, containerización, despliegue |
|
||||
| **DO** | Documentación | Técnica, usuario, API |
|
||||
| **SE** | Seguridad | Validaciones, autenticación, OWASP |
|
||||
|
||||
---
|
||||
|
||||
## Tabla de Actividades
|
||||
|
||||
### Fase 1: Análisis y Planificación (AN)
|
||||
|
||||
| # | Actividad | Detalle | Rol | Proceso |
|
||||
|---|-----------|---------|-----|---------|
|
||||
| 1.1 | Análisis de requisitos funcionales | Identificar y documentar los 9 requisitos del enunciado, criterios de aceptación por cada uno | Analista | AN |
|
||||
| 1.2 | Identificación de reglas de negocio | Documentar restricciones: máx 3 materias, 3 créditos/materia, 5 profesores con 2 materias c/u, restricción de profesor único | Analista | AN |
|
||||
| 1.3 | Definición de historias de usuario | Crear historias con formato "Como [rol] quiero [acción] para [beneficio]" con criterios de aceptación | Product Owner | AN |
|
||||
| 1.4 | Análisis de requisitos no funcionales | Definir: rendimiento (<200ms respuesta), seguridad (OWASP Top 10), usabilidad (responsive), mantenibilidad | Arquitecto | AN |
|
||||
| 1.5 | Identificación de riesgos técnicos | Mapear riesgos: complejidad de validaciones, integración frontend-backend, manejo de concurrencia | Líder Técnico | AN |
|
||||
|
||||
### Fase 2: Diseño de Arquitectura (DI)
|
||||
|
||||
| # | Actividad | Detalle | Rol | Proceso |
|
||||
|---|-----------|---------|-----|---------|
|
||||
| 2.1 | Diseño de arquitectura backend | Definir Clean Architecture: Domain, Application, Infrastructure, GraphQL. Diagrama de capas y dependencias | Arquitecto Backend | DI |
|
||||
| 2.2 | Diseño del modelo de dominio | Crear diagrama de entidades: Student, Subject, Professor, Enrollment. Definir agregados y value objects | Arquitecto Backend | DI |
|
||||
| 2.3 | Diseño de base de datos | Modelo E-R normalizado (3FN), índices, constraints, scripts DDL con integridad referencial | DBA/Arquitecto | DI |
|
||||
| 2.4 | Diseño de esquema GraphQL | Definir Types, Queries, Mutations, Inputs, Payloads. Diseñar resolvers y DataLoaders para N+1 | Arquitecto Backend | DI |
|
||||
| 2.5 | Diseño de arquitectura frontend | Definir estructura Angular: standalone components, signals, lazy loading, Apollo Client para GraphQL | Arquitecto Frontend | DI |
|
||||
| 2.6 | Diseño de componentes UI | Wireframes de pantallas: listado estudiantes, formulario inscripción, selección materias, vista compañeros | UI/UX Designer | DI |
|
||||
| 2.7 | Definición de contratos GraphQL | Schema GraphQL completo, DTOs de request/response, interfaces de servicios, contratos entre capas | Arquitecto | DI |
|
||||
| 2.8 | Diseño de estrategia de manejo de errores | Definir excepciones de dominio, error handling en GraphQL (Union types para errores), respuestas estandarizadas | Arquitecto | DI |
|
||||
|
||||
### Fase 3: Configuración del Entorno (DV)
|
||||
|
||||
| # | Actividad | Detalle | Rol | Proceso |
|
||||
|---|-----------|---------|-----|---------|
|
||||
| 3.1 | Inicialización del repositorio | Crear estructura de carpetas, .gitignore, README, CLAUDE.md con convenciones del proyecto | DevOps | DV |
|
||||
| 3.2 | Configuración solución .NET | Crear solución con 4 proyectos (Domain, Application, Infrastructure, GraphQL), referencias entre proyectos | Backend Dev | DV |
|
||||
| 3.3 | Configuración proyecto Angular | ng new con standalone, configurar ESLint, Prettier, paths aliases, Apollo Angular para GraphQL | Frontend Dev | DV |
|
||||
| 3.4 | Configuración de base de datos | Docker compose para SQL Server, scripts de inicialización, connection strings por ambiente | DevOps/DBA | DV |
|
||||
| 3.5 | Configuración de variables de entorno | User secrets para desarrollo, appsettings por ambiente, environment.ts en Angular | DevOps | DV |
|
||||
| 3.6 | Configuración de herramientas de calidad | EditorConfig, .NET analyzers, ESLint rules, Husky para pre-commit hooks | DevOps | DV |
|
||||
|
||||
### Fase 4: Desarrollo Backend (DE)
|
||||
|
||||
| # | Actividad | Detalle | Rol | Proceso |
|
||||
|---|-----------|---------|-----|---------|
|
||||
| 4.1 | Implementar capa Domain | Entidades (Student, Subject, Professor, Enrollment), Value Objects, interfaces de repositorios, excepciones de dominio | Backend Dev | DE |
|
||||
| 4.2 | Implementar reglas de negocio en Domain | Validaciones en entidades: EnrollmentPolicy (max 3 materias), ProfessorConstraint (no repetir profesor) | Backend Dev | DE |
|
||||
| 4.3 | Implementar capa Application - DTOs | Records inmutables para requests/responses, mappers con Mapster o extension methods | Backend Dev | DE |
|
||||
| 4.4 | Implementar capa Application - Servicios | StudentService, EnrollmentService con casos de uso: Create, Update, Delete, GetAll, GetById, GetClassmates | Backend Dev | DE |
|
||||
| 4.5 | Implementar validadores FluentValidation | Validators para CreateStudentInput, EnrollStudentInput con reglas de negocio | Backend Dev | DE |
|
||||
| 4.6 | Implementar capa Infrastructure - DbContext | Configurar EF Core 10, Fluent API para mapeos, configuraciones de entidades, seeding de datos iniciales | Backend Dev | DE |
|
||||
| 4.7 | Implementar capa Infrastructure - Repositorios | Repositorios genérico y específicos, Unit of Work, queries optimizadas con Include/ThenInclude | Backend Dev | DE |
|
||||
| 4.8 | Implementar GraphQL Types | StudentType, SubjectType, ProfessorType, EnrollmentType con HotChocolate | Backend Dev | DE |
|
||||
| 4.9 | Implementar GraphQL Queries | Resolvers para: students, student(id), subjects, availableSubjects(studentId), classmates(studentId), professors | Backend Dev | DE |
|
||||
| 4.10 | Implementar GraphQL Mutations | createStudent, updateStudent, deleteStudent, enrollStudent, unenrollStudent con Payloads de respuesta | Backend Dev | DE |
|
||||
| 4.11 | Implementar DataLoaders | DataLoaders para evitar N+1: StudentByIdDataLoader, SubjectByIdDataLoader, ProfessorByIdDataLoader | Backend Dev | DE |
|
||||
| 4.12 | Implementar middleware de errores | ExceptionHandlingMiddleware, mapeo de excepciones de dominio a errores GraphQL | Backend Dev | DE |
|
||||
| 4.13 | Configurar Dependency Injection | Registrar servicios por capa en Program.cs, configurar HotChocolate con servicios | Backend Dev | DE |
|
||||
| 4.14 | Implementar migraciones EF Core | Crear migración inicial, script de seed para profesores y materias predefinidas | Backend Dev | DE |
|
||||
|
||||
### Fase 5: Desarrollo Frontend (DE)
|
||||
|
||||
| # | Actividad | Detalle | Rol | Proceso |
|
||||
|---|-----------|---------|-----|---------|
|
||||
| 5.1 | Configurar Apollo Angular | Instalar apollo-angular, configurar ApolloModule con endpoint GraphQL, cache policies | Frontend Dev | DE |
|
||||
| 5.2 | Implementar GraphQL Queries en Angular | Archivos .graphql o queries en TypeScript para students, subjects, classmates | Frontend Dev | DE |
|
||||
| 5.3 | Implementar GraphQL Mutations en Angular | Mutations para crear/editar estudiante, inscribir/desinscribir materias | Frontend Dev | DE |
|
||||
| 5.4 | Implementar capa Core - Servicios | StudentService, SubjectService, EnrollmentService usando Apollo Client | Frontend Dev | DE |
|
||||
| 5.5 | Implementar capa Core - Interceptores | HttpErrorInterceptor para manejo global de errores, LoadingInterceptor para indicador de carga | Frontend Dev | DE |
|
||||
| 5.6 | Implementar capa Shared - Componentes UI | ButtonComponent, InputComponent, CardComponent, TableComponent, ModalComponent, AlertComponent | Frontend Dev | DE |
|
||||
| 5.7 | Implementar capa Shared - Directivas y Pipes | HighlightDirective, TooltipDirective, TruncatePipe, CreditsPipe | Frontend Dev | DE |
|
||||
| 5.8 | Implementar feature Students - Listado | Tabla con estudiantes usando Apollo watchQuery, búsqueda, paginación, acciones CRUD | Frontend Dev | DE |
|
||||
| 5.9 | Implementar feature Students - Formulario | Reactive form para crear/editar estudiante con validaciones, mutation de Apollo | Frontend Dev | DE |
|
||||
| 5.10 | Implementar feature Enrollment - Selección materias | Componente con query availableSubjects, validación visual de restricción de profesor, contador de créditos | Frontend Dev | DE |
|
||||
| 5.11 | Implementar feature Enrollment - Vista compañeros | Query classmates con listado de nombres por materia inscrita | Frontend Dev | DE |
|
||||
| 5.12 | Implementar gestión de estado | Signals para estado local, Apollo cache para estado del servidor, optimistic updates | Frontend Dev | DE |
|
||||
| 5.13 | Implementar manejo de errores UI | Toast notifications, estados de error en formularios, manejo de errores GraphQL | Frontend Dev | DE |
|
||||
| 5.14 | Implementar responsive design | CSS con mobile-first, breakpoints para tablet/desktop, Angular Material responsive | Frontend Dev | DE |
|
||||
| 5.15 | Implementar loading states | Skeletons, spinners, estados de carga con Apollo loading state | Frontend Dev | DE |
|
||||
|
||||
### Fase 6: Integración GraphQL (DE)
|
||||
|
||||
| # | Actividad | Detalle | Rol | Proceso |
|
||||
|---|-----------|---------|-----|---------|
|
||||
| 6.1 | Configurar CORS | Permitir origen del frontend en desarrollo y producción, headers para GraphQL | Backend Dev | DE |
|
||||
| 6.2 | Configurar Banana Cake Pop | Playground integrado de HotChocolate para testing de queries/mutations | Backend Dev | DE |
|
||||
| 6.3 | Implementar health checks | Endpoint /health para verificar conectividad a BD y servicios | Backend Dev | DE |
|
||||
| 6.4 | Configurar Apollo DevTools | Habilitar Apollo DevTools en desarrollo para debugging de queries y cache | Frontend Dev | DE |
|
||||
| 6.5 | Implementar caché de Apollo | Configurar cache policies: cache-first para datos estáticos, network-only para datos dinámicos | Frontend Dev | DE |
|
||||
| 6.6 | Generar tipos TypeScript | Usar GraphQL Code Generator para generar tipos desde el schema GraphQL | Frontend Dev | DE |
|
||||
|
||||
### Fase 7: Seguridad (SE)
|
||||
|
||||
| # | Actividad | Detalle | Rol | Proceso |
|
||||
|---|-----------|---------|-----|---------|
|
||||
| 7.1 | Implementar validación de entrada | Sanitización de inputs, validación en frontend y backend, prevención de inyección | Security Dev | SE |
|
||||
| 7.2 | Configurar headers de seguridad | Content-Security-Policy, X-Content-Type-Options, X-Frame-Options, HSTS | Security Dev | SE |
|
||||
| 7.3 | Implementar rate limiting | Limitar requests por IP para prevenir DoS, configurar en middleware GraphQL | Security Dev | SE |
|
||||
| 7.4 | Configurar query complexity | Limitar profundidad y complejidad de queries GraphQL para prevenir ataques | Security Dev | SE |
|
||||
| 7.5 | Validar OWASP Top 10 | Revisar: Injection, Broken Auth, XSS, Insecure Design, Security Misconfiguration | Security Dev | SE |
|
||||
| 7.6 | Configurar logging seguro | No loguear datos sensibles, structured logging con Serilog, niveles apropiados | Security Dev | SE |
|
||||
|
||||
### Fase 8: Testing (QA)
|
||||
|
||||
| # | Actividad | Detalle | Rol | Proceso |
|
||||
|---|-----------|---------|-----|---------|
|
||||
| 8.1 | Unit tests - Domain | Tests para reglas de negocio: validación max 3 materias, restricción profesor, cálculo créditos | QA/Backend Dev | QA |
|
||||
| 8.2 | Unit tests - Application | Tests para servicios, mappers, validators usando mocks de repositorios | QA/Backend Dev | QA |
|
||||
| 8.3 | Integration tests - GraphQL | Tests de queries y mutations con WebApplicationFactory, base de datos in-memory | QA/Backend Dev | QA |
|
||||
| 8.4 | Unit tests - Angular Components | Tests con Jest/Jasmine para componentes, servicios con Apollo testing utilities | QA/Frontend Dev | QA |
|
||||
| 8.5 | E2E tests | Tests con Playwright para flujos críticos: registro estudiante, inscripción materias | QA | QA |
|
||||
| 8.6 | Tests de reglas de negocio | Casos de prueba específicos para restricciones: intentar 4ta materia, mismo profesor | QA | QA |
|
||||
| 8.7 | Code review | Revisión de código aplicando checklist: SOLID, Clean Code, convenciones, seguridad | Líder Técnico | QA |
|
||||
| 8.8 | Análisis de código estático | Ejecutar analyzers .NET, ESLint, corregir code smells, vulnerabilidades, duplicación | QA | QA |
|
||||
|
||||
### Fase 9: Optimización y Rendimiento (DE)
|
||||
|
||||
| # | Actividad | Detalle | Rol | Proceso |
|
||||
|---|-----------|---------|-----|---------|
|
||||
| 9.1 | Optimizar queries EF Core | Revisar N+1 (usar DataLoaders), AsNoTracking para lecturas, proyecciones con Select, índices en BD | Backend Dev | DE |
|
||||
| 9.2 | Implementar caché backend | Response caching para queries estáticas, memory cache para datos frecuentes | Backend Dev | DE |
|
||||
| 9.3 | Optimizar bundle Angular | Lazy loading de rutas, tree shaking, análisis de bundle size | Frontend Dev | DE |
|
||||
| 9.4 | Optimizar rendimiento UI | OnPush change detection, trackBy en ngFor, debounce en búsquedas | Frontend Dev | DE |
|
||||
| 9.5 | Comprimir assets | Minificación CSS/JS, compresión gzip/brotli | DevOps | DE |
|
||||
|
||||
### Fase 10: Documentación (DO)
|
||||
|
||||
| # | Actividad | Detalle | Rol | Proceso |
|
||||
|---|-----------|---------|-----|---------|
|
||||
| 10.1 | Documentar arquitectura | Diagrama C4 (Context, Container, Component), decisiones de arquitectura (ADR) | Arquitecto | DO |
|
||||
| 10.2 | Documentar esquema GraphQL | Schema completo con descripciones, ejemplos de queries/mutations en Banana Cake Pop | Backend Dev | DO |
|
||||
| 10.3 | Documentar modelo de datos | Diagrama E-R, diccionario de datos, scripts de creación comentados | DBA | DO |
|
||||
| 10.4 | README del proyecto | Instrucciones de instalación, configuración, ejecución, estructura del proyecto | Líder Técnico | DO |
|
||||
| 10.5 | Documentar decisiones técnicas | Por qué Clean Architecture, por qué GraphQL vs REST, por qué Signals vs RxJS | Arquitecto | DO |
|
||||
| 10.6 | Manual de despliegue | Pasos para deploy, variables de entorno requeridas, checklist pre-producción | DevOps | DO |
|
||||
|
||||
### Fase 11: Entrega y Despliegue (DV)
|
||||
|
||||
| # | Actividad | Detalle | Rol | Proceso |
|
||||
|---|-----------|---------|-----|---------|
|
||||
| 11.1 | Configurar Dockerfile GraphQL | Multi-stage build, imagen optimizada, health check, non-root user | DevOps | DV |
|
||||
| 11.2 | Configurar Dockerfile frontend | Build con Node, serve con Nginx, configuración de proxy para GraphQL | DevOps | DV |
|
||||
| 11.3 | Crear docker-compose | Orquestación de GraphQL API, frontend, SQL Server, configuración de red y volúmenes | DevOps | DV |
|
||||
| 11.4 | Pruebas de despliegue | Verificar funcionamiento end-to-end en ambiente containerizado | DevOps/QA | DV |
|
||||
| 11.5 | Preparar entregables | Empaquetar código fuente, scripts BD, documentación, instrucciones de ejecución | Líder Técnico | DV |
|
||||
| 11.6 | Validación final | Checklist de requisitos cumplidos, demo funcional, revisión de calidad | Líder Técnico | DV |
|
||||
|
||||
---
|
||||
|
||||
## Resumen por Proceso
|
||||
|
||||
| Proceso | Actividades | Peso |
|
||||
|---------|-------------|------|
|
||||
| Análisis (AN) | 5 | 5% |
|
||||
| Diseño (DI) | 8 | 15% |
|
||||
| DevOps (DV) | 12 | 10% |
|
||||
| Desarrollo Backend (DE) | 14 | 30% |
|
||||
| Desarrollo Frontend (DE) | 15 | 25% |
|
||||
| Seguridad (SE) | 6 | 5% |
|
||||
| Testing (QA) | 8 | 5% |
|
||||
| Documentación (DO) | 6 | 5% |
|
||||
| **TOTAL** | **74** | **100%** |
|
||||
|
||||
---
|
||||
|
||||
## Criterios de Calidad Senior
|
||||
|
||||
### Competencias Demostradas
|
||||
|
||||
| Área | Evidencia |
|
||||
|------|-----------|
|
||||
| **Arquitectura** | Clean Architecture, separación de concerns, principios SOLID |
|
||||
| **GraphQL** | Schema bien diseñado, DataLoaders para N+1, error handling |
|
||||
| **Código Limpio** | Naming conventions, SRP, métodos pequeños, sin duplicación |
|
||||
| **Testing** | Cobertura de reglas críticas, tests unitarios y de integración |
|
||||
| **Seguridad** | OWASP compliance, validación de inputs, query complexity limits |
|
||||
| **Rendimiento** | Queries optimizadas, caching estratégico, Apollo cache |
|
||||
| **Mantenibilidad** | Código autodocumentado, arquitectura desacoplada, DTOs |
|
||||
| **DevOps** | Containerización, configuración por ambiente |
|
||||
| **Documentación** | Schema GraphQL documentado, README completo |
|
||||
|
||||
### Diferenciadores Senior vs Junior
|
||||
|
||||
| Aspecto | Junior | Senior (Este proyecto) |
|
||||
|---------|--------|------------------------|
|
||||
| Arquitectura | Monolítico, acoplado | Clean Architecture, capas bien definidas |
|
||||
| API | REST básico | GraphQL con HotChocolate, DataLoaders |
|
||||
| Validaciones | Solo en frontend | Frontend + Backend + Domain |
|
||||
| Errores | Try-catch genéricos | Excepciones de dominio, Union types para errores |
|
||||
| Testing | Manual o ninguno | Unit + Integration + E2E automatizados |
|
||||
| Estado Frontend | Variables globales | Signals + Apollo Cache |
|
||||
| BD | EF directo en controller | Repository + Unit of Work + Specifications |
|
||||
| Seguridad | Ninguna | OWASP, headers, rate limiting, query complexity |
|
||||
|
||||
---
|
||||
|
||||
## Orden de Ejecución Recomendado
|
||||
|
||||
```
|
||||
Bloque 1: Fundamentos
|
||||
├── AN (1.1-1.5) → Requisitos claros
|
||||
├── DI (2.1-2.8) → Arquitectura definida
|
||||
└── DV (3.1-3.6) → Ambiente configurado
|
||||
|
||||
Bloque 2: Backend Core
|
||||
├── DE (4.1-4.7) → Domain + Application + Infrastructure
|
||||
└── DE (4.8-4.14) → GraphQL API funcional
|
||||
|
||||
Bloque 3: Frontend Core
|
||||
├── DE (5.1-5.7) → Core + Shared + Apollo config
|
||||
└── DE (5.8-5.15) → Features completas
|
||||
|
||||
Bloque 4: Integración
|
||||
└── DE (6.1-6.6) → Frontend ↔ Backend conectados
|
||||
|
||||
Bloque 5: Calidad
|
||||
├── SE (7.1-7.6) → Seguridad implementada
|
||||
├── QA (8.1-8.8) → Tests ejecutados
|
||||
└── DE (9.1-9.5) → Optimizaciones aplicadas
|
||||
|
||||
Bloque 6: Entrega
|
||||
├── DO (10.1-10.6) → Documentación completa
|
||||
└── DV (11.1-11.6) → Despliegue validado
|
||||
```
|
||||
|
||||
> **Nota:** Ejecutar los bloques secuencialmente. Dentro de cada bloque, las actividades pueden paralelizarse según disponibilidad de recursos.
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
# PRUEBA TECNICA APLICACION WEB
|
||||
|
||||
1. Los resultados deben ser enviados en un documento adjunto.
|
||||
2. Los entregables deben venir adjuntos.
|
||||
|
||||
## El test consiste en:
|
||||
|
||||
Una aplicación para registro de estudiantes:
|
||||
|
||||
1. Realizar un CRUD que le permita a un usuario realizar un registro en línea.
|
||||
2. El estudiante se adhiere a un programa de créditos
|
||||
3. Existen 10 materias
|
||||
4. Cada materia equivale a 3 créditos.
|
||||
5. El estudiante sólo podrá seleccionar 3 materias.
|
||||
6. Hay 5 profesores que dictan 2 materias cada uno.
|
||||
7. El estudiante no podrá tener clases con el mismo profesor.
|
||||
8. Cada estudiante puede ver en línea los registros de otros estudiantes.
|
||||
9. El estudiante podrá ver sólo el nombre de los alumnos con quienes compartirá cada clase.
|
||||
|
||||
## Entregables:
|
||||
|
||||
Una aplicación web o Cliente servidor
|
||||
Base de datos o scripts para su creación en MySql / SQL
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
# ADR-001: Clean Architecture
|
||||
|
||||
**Estado:** Aceptado
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
## Contexto
|
||||
|
||||
Necesitamos una arquitectura que permita:
|
||||
- Testabilidad de reglas de negocio
|
||||
- Independencia de frameworks
|
||||
- Mantenibilidad a largo plazo
|
||||
- Separación clara de responsabilidades
|
||||
|
||||
## Decisión
|
||||
|
||||
Adoptar **Clean Architecture** con 4 capas: Domain, Application, Adapters, Host.
|
||||
|
||||
```
|
||||
Host → Adapters → Application → Domain
|
||||
```
|
||||
|
||||
## Consecuencias
|
||||
|
||||
### Positivas
|
||||
- Domain sin dependencias externas (puro C#)
|
||||
- Reglas de negocio testeables sin mocks de infraestructura
|
||||
- Fácil cambiar ORM o base de datos
|
||||
- Fácil cambiar de REST a GraphQL (o viceversa)
|
||||
|
||||
### Negativas
|
||||
- Mayor cantidad de archivos/proyectos
|
||||
- Curva de aprendizaje inicial
|
||||
- Overhead para proyectos muy pequeños
|
||||
|
||||
## Alternativas Consideradas
|
||||
|
||||
| Alternativa | Razón de Descarte |
|
||||
|-------------|-------------------|
|
||||
| N-Layer tradicional | Alto acoplamiento, difícil testear |
|
||||
| Vertical Slices | Menor separación de concerns |
|
||||
| Monolítico simple | No escala con complejidad |
|
||||
|
||||
## Referencias
|
||||
|
||||
- [Clean Architecture - Robert C. Martin](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html)
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# ADR-002: GraphQL vs REST
|
||||
|
||||
**Estado:** Aceptado
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
## Contexto
|
||||
|
||||
La aplicación tiene relaciones complejas:
|
||||
- Estudiantes → Inscripciones → Materias → Profesores
|
||||
- Consultas como "materias disponibles" requieren múltiples joins
|
||||
- Frontend necesita flexibilidad en datos solicitados
|
||||
|
||||
## Decisión
|
||||
|
||||
Usar **GraphQL** con HotChocolate como API.
|
||||
|
||||
## Consecuencias
|
||||
|
||||
### Positivas
|
||||
- **No over-fetching:** Cliente pide solo campos necesarios
|
||||
- **No under-fetching:** Una query obtiene datos relacionados
|
||||
- **Schema tipado:** Contrato explícito frontend-backend
|
||||
- **Playground incluido:** Banana Cake Pop para testing
|
||||
- **DataLoaders:** Resuelve N+1 automáticamente
|
||||
|
||||
### Negativas
|
||||
- Complejidad adicional vs REST simple
|
||||
- Curva de aprendizaje GraphQL
|
||||
- Requiere configurar query complexity limits
|
||||
- Cache más complejo que HTTP caching
|
||||
|
||||
## Ejemplo Comparativo
|
||||
|
||||
### REST (múltiples requests)
|
||||
```
|
||||
GET /students/1
|
||||
GET /students/1/enrollments
|
||||
GET /subjects/1
|
||||
GET /professors/1
|
||||
```
|
||||
|
||||
### GraphQL (una query)
|
||||
```graphql
|
||||
query {
|
||||
student(id: 1) {
|
||||
name
|
||||
enrollments {
|
||||
subject {
|
||||
name
|
||||
professor { name }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Alternativas
|
||||
|
||||
| Alternativa | Razón de Descarte |
|
||||
|-------------|-------------------|
|
||||
| REST | Over/under-fetching, múltiples endpoints |
|
||||
| OData | Menos flexible, menos ecosistema |
|
||||
| gRPC | No ideal para frontend web |
|
||||
|
||||
## Referencias
|
||||
|
||||
- [HotChocolate Docs](https://chillicream.com/docs/hotchocolate)
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# ADR-003: Signals vs RxJS para Estado Local
|
||||
|
||||
**Estado:** Aceptado
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
## Contexto
|
||||
|
||||
Angular 21 introduce Signals como alternativa a RxJS para estado reactivo.
|
||||
Necesitamos decidir el enfoque para manejo de estado en el frontend.
|
||||
|
||||
## Decisión
|
||||
|
||||
Usar **Signals para estado local** + **Apollo Client para estado del servidor**.
|
||||
|
||||
```typescript
|
||||
// Estado local con Signals
|
||||
students = signal<Student[]>([]);
|
||||
loading = signal(true);
|
||||
|
||||
// Estado del servidor con Apollo
|
||||
this.apollo.watchQuery<GetStudentsQuery>({...})
|
||||
```
|
||||
|
||||
## Consecuencias
|
||||
|
||||
### Positivas
|
||||
- **Simplicidad:** Signals más intuitivos que BehaviorSubject
|
||||
- **Performance:** Integración nativa con OnPush
|
||||
- **Menos boilerplate:** No requiere async pipe en templates
|
||||
- **Type-safe:** Mejor inferencia de tipos
|
||||
|
||||
### Negativas
|
||||
- Tecnología relativamente nueva
|
||||
- Menos operadores que RxJS
|
||||
- Apollo aún usa Observables internamente
|
||||
|
||||
## Patrón Adoptado
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class StudentListComponent {
|
||||
// Estado local
|
||||
students = signal<Student[]>([]);
|
||||
loading = signal(true);
|
||||
|
||||
// Suscripción a Apollo
|
||||
ngOnInit() {
|
||||
this.studentService.getStudents().subscribe(({ data, loading }) => {
|
||||
this.students.set(data);
|
||||
this.loading.set(loading);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Alternativas
|
||||
|
||||
| Alternativa | Razón de Descarte |
|
||||
|-------------|-------------------|
|
||||
| RxJS puro | Mayor complejidad, más boilerplate |
|
||||
| NgRx | Overkill para esta aplicación |
|
||||
| Akita | Dependencia adicional innecesaria |
|
||||
|
||||
## Referencias
|
||||
|
||||
- [Angular Signals](https://angular.dev/guide/signals)
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
# ADR-004: Estrategia de Validación en 3 Capas
|
||||
|
||||
**Estado:** Aceptado
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
## Contexto
|
||||
|
||||
Las reglas de negocio críticas son:
|
||||
- Máximo 3 materias por estudiante (9 créditos)
|
||||
- No repetir profesor en inscripciones
|
||||
- Datos de entrada válidos (email, nombre)
|
||||
|
||||
## Decisión
|
||||
|
||||
Validar en **3 capas** con responsabilidades distintas:
|
||||
|
||||
| Capa | Responsabilidad | Tecnología |
|
||||
|------|-----------------|------------|
|
||||
| Frontend | UX, feedback rápido | Reactive Forms |
|
||||
| Application | Estructura de datos, sanitización | FluentValidation |
|
||||
| Domain | Reglas de negocio puras | Domain Services |
|
||||
|
||||
## Implementación
|
||||
|
||||
### Frontend (UX)
|
||||
```typescript
|
||||
this.form = this.fb.group({
|
||||
name: ['', [Validators.required, Validators.minLength(3)]],
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
});
|
||||
```
|
||||
|
||||
### Application (Sanitización + XSS)
|
||||
```csharp
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty()
|
||||
.Must(NotContainDangerousContent); // Previene XSS
|
||||
```
|
||||
|
||||
### Domain (Negocio)
|
||||
```csharp
|
||||
public void ValidateEnrollment(Student student, Subject subject)
|
||||
{
|
||||
if (student.Enrollments.Count >= 3)
|
||||
throw new MaxEnrollmentsExceededException();
|
||||
|
||||
if (student.HasProfessor(subject.ProfessorId))
|
||||
throw new SameProfessorConstraintException();
|
||||
}
|
||||
```
|
||||
|
||||
## Consecuencias
|
||||
|
||||
### Positivas
|
||||
- Defensa en profundidad
|
||||
- Separación de responsabilidades
|
||||
- UX mejorada (errores rápidos)
|
||||
- Seguridad garantizada (backend siempre valida)
|
||||
|
||||
### Negativas
|
||||
- Duplicación parcial de reglas
|
||||
- Mantener sincronizadas las validaciones
|
||||
|
||||
## Regla de Oro
|
||||
|
||||
> **Nunca confíes en el frontend.** El backend SIEMPRE debe validar.
|
||||
|
||||
## Referencias
|
||||
|
||||
- [OWASP Input Validation](https://owasp.org/www-community/Input_Validation)
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Architecture Decision Records (ADR)
|
||||
|
||||
Registro de decisiones arquitectónicas del proyecto.
|
||||
|
||||
## Índice
|
||||
|
||||
| ADR | Título | Estado |
|
||||
|-----|--------|--------|
|
||||
| [ADR-001](ADR-001-clean-architecture.md) | Clean Architecture | Aceptado |
|
||||
| [ADR-002](ADR-002-graphql-vs-rest.md) | GraphQL vs REST | Aceptado |
|
||||
| [ADR-003](ADR-003-angular-signals.md) | Signals vs RxJS | Aceptado |
|
||||
| [ADR-004](ADR-004-validation-strategy.md) | Estrategia de Validación | Aceptado |
|
||||
|
||||
## Formato ADR
|
||||
|
||||
Cada ADR sigue el formato:
|
||||
|
||||
1. **Título:** Nombre descriptivo
|
||||
2. **Estado:** Propuesto | Aceptado | Deprecado | Reemplazado
|
||||
3. **Contexto:** Por qué se necesita esta decisión
|
||||
4. **Decisión:** Qué se decidió
|
||||
5. **Consecuencias:** Positivas y negativas
|
||||
6. **Alternativas:** Opciones consideradas y descartadas
|
||||
|
||||
## Convenciones
|
||||
|
||||
- Nuevos ADRs: `ADR-XXX-nombre-descriptivo.md`
|
||||
- Nunca modificar ADRs aceptados (crear uno nuevo que lo reemplace)
|
||||
- Mantener este índice actualizado
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
@startuml use-cases
|
||||
!theme plain
|
||||
skinparam actorStyle awesome
|
||||
skinparam packageStyle rectangle
|
||||
skinparam usecaseBackgroundColor #F8F9FA
|
||||
skinparam usecaseBorderColor #495057
|
||||
skinparam actorBackgroundColor #007AFF
|
||||
|
||||
title Sistema de Registro de Estudiantes - Diagrama de Casos de Uso
|
||||
|
||||
actor "Estudiante" as student
|
||||
|
||||
rectangle "Sistema de Inscripción" {
|
||||
usecase "Registrarse en el sistema" as UC1
|
||||
usecase "Iniciar sesión" as UC2
|
||||
usecase "Ver materias disponibles" as UC3
|
||||
usecase "Inscribirse en materia" as UC4
|
||||
usecase "Cancelar inscripción" as UC5
|
||||
usecase "Ver mis inscripciones" as UC6
|
||||
usecase "Ver compañeros de clase" as UC7
|
||||
usecase "Actualizar perfil" as UC8
|
||||
|
||||
usecase "Validar límite de créditos\n(máx 9 créditos)" as UC4a
|
||||
usecase "Validar restricción de profesor\n(no repetir profesor)" as UC4b
|
||||
}
|
||||
|
||||
student --> UC1
|
||||
student --> UC2
|
||||
student --> UC3
|
||||
student --> UC4
|
||||
student --> UC5
|
||||
student --> UC6
|
||||
student --> UC7
|
||||
student --> UC8
|
||||
|
||||
UC4 ..> UC4a : <<include>>
|
||||
UC4 ..> UC4b : <<include>>
|
||||
|
||||
note right of UC4
|
||||
Reglas de negocio:
|
||||
- Máximo 3 materias (9 créditos)
|
||||
- No puede tener 2 materias
|
||||
del mismo profesor
|
||||
end note
|
||||
|
||||
note right of UC7
|
||||
Solo muestra nombres
|
||||
de compañeros por materia
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
|
@ -0,0 +1,94 @@
|
|||
@startuml domain-model
|
||||
!theme plain
|
||||
skinparam classAttributeIconSize 0
|
||||
skinparam classFontStyle bold
|
||||
skinparam classBackgroundColor #F8F9FA
|
||||
skinparam classBorderColor #495057
|
||||
|
||||
title Sistema de Registro de Estudiantes - Modelo de Dominio
|
||||
|
||||
package "Domain" {
|
||||
|
||||
class Student <<Entity>> {
|
||||
- id: int
|
||||
- name: string
|
||||
- email: Email
|
||||
- enrollments: List<Enrollment>
|
||||
--
|
||||
+ getTotalCredits(): int
|
||||
+ canEnrollIn(subject: Subject): bool
|
||||
+ enroll(subject: Subject): Enrollment
|
||||
+ unenroll(enrollmentId: int): void
|
||||
}
|
||||
|
||||
class Subject <<Entity>> {
|
||||
- id: int
|
||||
- name: string
|
||||
- credits: int {= 3}
|
||||
- professorId: int
|
||||
--
|
||||
+ getProfessor(): Professor
|
||||
}
|
||||
|
||||
class Professor <<Entity>> {
|
||||
- id: int
|
||||
- name: string
|
||||
- subjects: List<Subject>
|
||||
--
|
||||
+ getSubjects(): List<Subject>
|
||||
}
|
||||
|
||||
class Enrollment <<Entity>> {
|
||||
- id: int
|
||||
- studentId: int
|
||||
- subjectId: int
|
||||
- enrolledAt: DateTime
|
||||
--
|
||||
+ getStudent(): Student
|
||||
+ getSubject(): Subject
|
||||
}
|
||||
|
||||
class Email <<Value Object>> {
|
||||
- value: string
|
||||
--
|
||||
+ {static} create(value: string): Email
|
||||
- validate(value: string): void
|
||||
}
|
||||
|
||||
class EnrollmentDomainService <<Domain Service>> {
|
||||
--
|
||||
+ validateEnrollment(student: Student, subject: Subject): void
|
||||
- checkMaxEnrollments(student: Student): void
|
||||
- checkProfessorConstraint(student: Student, subject: Subject): void
|
||||
}
|
||||
|
||||
' Relaciones
|
||||
Student "1" *-- "0..3" Enrollment : tiene
|
||||
Subject "1" *-- "0..*" Enrollment : inscripciones
|
||||
Professor "1" *-- "2" Subject : imparte
|
||||
Student *-- Email : email
|
||||
|
||||
EnrollmentDomainService ..> Student : valida
|
||||
EnrollmentDomainService ..> Subject : valida
|
||||
}
|
||||
|
||||
note bottom of Student
|
||||
<b>Invariantes:</b>
|
||||
- Máximo 3 inscripciones
|
||||
- Email válido y único
|
||||
- No repetir profesor
|
||||
end note
|
||||
|
||||
note bottom of Subject
|
||||
<b>Invariantes:</b>
|
||||
- Créditos = 3 (fijo)
|
||||
- Pertenece a un profesor
|
||||
end note
|
||||
|
||||
note right of Professor
|
||||
Cada profesor
|
||||
imparte exactamente
|
||||
2 materias
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
After Width: | Height: | Size: 24 KiB |
|
|
@ -0,0 +1,91 @@
|
|||
@startuml sequence-enrollment
|
||||
!theme plain
|
||||
skinparam sequenceMessageAlign center
|
||||
skinparam responseMessageBelowArrow true
|
||||
skinparam sequenceParticipantBackgroundColor #F8F9FA
|
||||
skinparam sequenceParticipantBorderColor #495057
|
||||
|
||||
title Secuencia: Inscripción de Estudiante en Materia
|
||||
|
||||
actor "Estudiante" as user
|
||||
participant "Frontend\n(Angular)" as frontend
|
||||
participant "API GraphQL\n(HotChocolate)" as api
|
||||
participant "EnrollStudentHandler" as handler
|
||||
participant "EnrollmentDomainService" as domainService
|
||||
participant "StudentRepository" as studentRepo
|
||||
participant "SubjectRepository" as subjectRepo
|
||||
participant "EnrollmentRepository" as enrollRepo
|
||||
database "SQL Server" as db
|
||||
|
||||
== Solicitud de Inscripción ==
|
||||
|
||||
user -> frontend : Selecciona materia\ny hace clic en "Inscribir"
|
||||
activate frontend
|
||||
|
||||
frontend -> api : mutation enrollStudent(\n studentId, subjectId)
|
||||
activate api
|
||||
|
||||
api -> handler : Handle(EnrollStudentCommand)
|
||||
activate handler
|
||||
|
||||
== Obtención de Datos ==
|
||||
|
||||
handler -> studentRepo : GetByIdWithEnrollmentsAsync(studentId)
|
||||
activate studentRepo
|
||||
studentRepo -> db : SELECT Student + Enrollments
|
||||
db --> studentRepo : Student data
|
||||
studentRepo --> handler : Student
|
||||
deactivate studentRepo
|
||||
|
||||
handler -> subjectRepo : GetByIdAsync(subjectId)
|
||||
activate subjectRepo
|
||||
subjectRepo -> db : SELECT Subject
|
||||
db --> subjectRepo : Subject data
|
||||
subjectRepo --> handler : Subject
|
||||
deactivate subjectRepo
|
||||
|
||||
== Validación de Reglas de Negocio ==
|
||||
|
||||
handler -> domainService : ValidateEnrollment(student, subject)
|
||||
activate domainService
|
||||
|
||||
domainService -> domainService : CheckMaxEnrollments()\n[máx 3 materias]
|
||||
|
||||
alt Estudiante tiene 3 materias
|
||||
domainService --> handler : throw MaxEnrollmentsExceededException
|
||||
handler --> api : Error: "Límite de materias alcanzado"
|
||||
api --> frontend : { errors: [...] }
|
||||
frontend --> user : Muestra mensaje de error
|
||||
end
|
||||
|
||||
domainService -> domainService : CheckProfessorConstraint()\n[no repetir profesor]
|
||||
|
||||
alt Ya tiene materia con el profesor
|
||||
domainService --> handler : throw SameProfessorConstraintException
|
||||
handler --> api : Error: "Ya tienes materia con este profesor"
|
||||
api --> frontend : { errors: [...] }
|
||||
frontend --> user : Muestra mensaje de error
|
||||
end
|
||||
|
||||
domainService --> handler : Validación OK
|
||||
deactivate domainService
|
||||
|
||||
== Persistencia ==
|
||||
|
||||
handler -> enrollRepo : AddAsync(enrollment)
|
||||
activate enrollRepo
|
||||
enrollRepo -> db : INSERT Enrollment
|
||||
db --> enrollRepo : OK
|
||||
enrollRepo --> handler : Enrollment
|
||||
deactivate enrollRepo
|
||||
|
||||
handler --> api : EnrollmentPayload
|
||||
deactivate handler
|
||||
|
||||
api --> frontend : { enrollment: {...} }
|
||||
deactivate api
|
||||
|
||||
frontend --> user : Muestra confirmación:\n"Inscrito en [materia]"
|
||||
deactivate frontend
|
||||
|
||||
@enduml
|
||||
|
After Width: | Height: | Size: 34 KiB |
|
|
@ -0,0 +1,134 @@
|
|||
@startuml components
|
||||
!theme plain
|
||||
skinparam componentStyle uml2
|
||||
skinparam packageStyle rectangle
|
||||
skinparam componentBackgroundColor #F8F9FA
|
||||
skinparam componentBorderColor #495057
|
||||
skinparam packageBackgroundColor #FFFFFF
|
||||
skinparam packageBorderColor #DEE2E6
|
||||
|
||||
title Sistema de Registro de Estudiantes - Arquitectura de Componentes
|
||||
|
||||
package "Frontend (Angular 21)" as frontend {
|
||||
[App Component] as app
|
||||
[Student List] as studentList
|
||||
[Student Form] as studentForm
|
||||
[Enrollment Page] as enrollPage
|
||||
[Classmates Page] as classmates
|
||||
|
||||
package "Core" {
|
||||
[Apollo Client] as apollo
|
||||
[Student Service] as studentSvc
|
||||
[Enrollment Service] as enrollSvc
|
||||
[Connectivity Service] as connSvc
|
||||
[Error Handler] as errorHandler
|
||||
}
|
||||
|
||||
package "Shared" {
|
||||
[Connectivity Overlay] as overlay
|
||||
[Loading Spinner] as spinner
|
||||
[Confirm Dialog] as dialog
|
||||
}
|
||||
}
|
||||
|
||||
package "Backend (.NET 10)" as backend {
|
||||
|
||||
package "Host" as host {
|
||||
[Program.cs] as program
|
||||
[DI Container] as di
|
||||
}
|
||||
|
||||
package "Adapters" as adapters {
|
||||
package "Driving (Primary)" {
|
||||
[GraphQL API\n(HotChocolate)] as graphql
|
||||
[Query] as query
|
||||
[Mutation] as mutation
|
||||
[Types] as types
|
||||
}
|
||||
|
||||
package "Driven (Secondary)" {
|
||||
[Repositories] as repos
|
||||
[DataLoaders] as loaders
|
||||
[DbContext] as dbContext
|
||||
}
|
||||
}
|
||||
|
||||
package "Application" as application {
|
||||
[Commands] as commands
|
||||
[Queries] as queries
|
||||
[Handlers] as handlers
|
||||
[Validators] as validators
|
||||
[DTOs] as dtos
|
||||
}
|
||||
|
||||
package "Domain" as domain {
|
||||
[Entities] as entities
|
||||
[Value Objects] as valueObjects
|
||||
[Domain Services] as domainSvc
|
||||
[Ports (Interfaces)] as ports
|
||||
}
|
||||
}
|
||||
|
||||
database "SQL Server 2022" as sqlserver {
|
||||
[Students]
|
||||
[Subjects]
|
||||
[Professors]
|
||||
[Enrollments]
|
||||
}
|
||||
|
||||
cloud "Browser" as browser
|
||||
|
||||
' Conexiones Frontend
|
||||
browser --> app
|
||||
app --> studentList
|
||||
app --> studentForm
|
||||
app --> enrollPage
|
||||
app --> classmates
|
||||
app --> overlay
|
||||
|
||||
studentList --> studentSvc
|
||||
studentForm --> studentSvc
|
||||
enrollPage --> enrollSvc
|
||||
classmates --> enrollSvc
|
||||
overlay --> connSvc
|
||||
|
||||
studentSvc --> apollo
|
||||
enrollSvc --> apollo
|
||||
connSvc ..> errorHandler
|
||||
|
||||
' Conexiones Backend
|
||||
apollo --> graphql : HTTP/GraphQL
|
||||
|
||||
graphql --> query
|
||||
graphql --> mutation
|
||||
query --> handlers
|
||||
mutation --> handlers
|
||||
handlers --> validators
|
||||
handlers --> commands
|
||||
handlers --> queries
|
||||
|
||||
commands --> domainSvc
|
||||
queries --> repos
|
||||
domainSvc --> entities
|
||||
domainSvc --> valueObjects
|
||||
|
||||
repos --> dbContext
|
||||
loaders --> dbContext
|
||||
dbContext --> sqlserver
|
||||
|
||||
' Implementación de puertos
|
||||
repos ..|> ports : implements
|
||||
|
||||
note right of domain
|
||||
<b>Regla de Dependencia:</b>
|
||||
Domain no depende
|
||||
de capas externas
|
||||
end note
|
||||
|
||||
note bottom of graphql
|
||||
Endpoints:
|
||||
- /graphql
|
||||
- /health
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
After Width: | Height: | Size: 51 KiB |
|
|
@ -0,0 +1,72 @@
|
|||
@startuml entity-relationship
|
||||
!theme plain
|
||||
skinparam linetype ortho
|
||||
skinparam classBackgroundColor #F8F9FA
|
||||
skinparam classBorderColor #495057
|
||||
|
||||
title Sistema de Registro de Estudiantes - Diagrama Entidad-Relación
|
||||
|
||||
entity "Students" as students {
|
||||
* **Id** : int <<PK>>
|
||||
--
|
||||
* Name : nvarchar(100)
|
||||
* Email : nvarchar(255) <<unique>>
|
||||
* CreatedAt : datetime2
|
||||
UpdatedAt : datetime2
|
||||
}
|
||||
|
||||
entity "Professors" as professors {
|
||||
* **Id** : int <<PK>>
|
||||
--
|
||||
* Name : nvarchar(100)
|
||||
}
|
||||
|
||||
entity "Subjects" as subjects {
|
||||
* **Id** : int <<PK>>
|
||||
--
|
||||
* Name : nvarchar(100)
|
||||
* Credits : int {= 3}
|
||||
* **ProfessorId** : int <<FK>>
|
||||
}
|
||||
|
||||
entity "Enrollments" as enrollments {
|
||||
* **Id** : int <<PK>>
|
||||
--
|
||||
* **StudentId** : int <<FK>>
|
||||
* **SubjectId** : int <<FK>>
|
||||
* EnrolledAt : datetime2
|
||||
--
|
||||
<<unique>> (StudentId, SubjectId)
|
||||
}
|
||||
|
||||
' Relaciones
|
||||
students ||--o{ enrollments : "tiene"
|
||||
subjects ||--o{ enrollments : "inscripciones"
|
||||
professors ||--|| subjects : "imparte 2"
|
||||
|
||||
note right of students
|
||||
<b>Restricciones:</b>
|
||||
- Email único
|
||||
- Máximo 3 enrollments
|
||||
end note
|
||||
|
||||
note right of subjects
|
||||
<b>Datos iniciales:</b>
|
||||
10 materias
|
||||
3 créditos cada una
|
||||
end note
|
||||
|
||||
note right of professors
|
||||
<b>Datos iniciales:</b>
|
||||
5 profesores
|
||||
2 materias cada uno
|
||||
end note
|
||||
|
||||
note bottom of enrollments
|
||||
<b>Reglas de negocio:</b>
|
||||
- (StudentId, SubjectId) único
|
||||
- Estudiante no puede tener
|
||||
2 materias del mismo profesor
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -0,0 +1,49 @@
|
|||
@startuml state-enrollment
|
||||
!theme plain
|
||||
skinparam stateBackgroundColor #F8F9FA
|
||||
skinparam stateBorderColor #495057
|
||||
|
||||
title Estado de Inscripción del Estudiante
|
||||
|
||||
[*] --> SinMaterias : Registro inicial
|
||||
|
||||
state "Sin Materias" as SinMaterias {
|
||||
state "0 créditos" as cred0
|
||||
}
|
||||
|
||||
state "Inscripción Parcial" as Parcial {
|
||||
state "3 créditos\n(1 materia)" as cred3
|
||||
state "6 créditos\n(2 materias)" as cred6
|
||||
}
|
||||
|
||||
state "Inscripción Completa" as Completa {
|
||||
state "9 créditos\n(3 materias)" as cred9
|
||||
}
|
||||
|
||||
SinMaterias --> Parcial : inscribir(materia)
|
||||
Parcial --> Parcial : inscribir(materia)\n[créditos < 9]
|
||||
Parcial --> Completa : inscribir(materia)\n[créditos = 6]
|
||||
Completa --> Parcial : cancelar(inscripción)
|
||||
Parcial --> SinMaterias : cancelar(inscripción)\n[única materia]
|
||||
|
||||
note right of Completa
|
||||
No puede inscribir
|
||||
más materias
|
||||
end note
|
||||
|
||||
note left of Parcial
|
||||
<b>Validaciones en cada inscripción:</b>
|
||||
- Límite de créditos
|
||||
- No repetir profesor
|
||||
- Materia no duplicada
|
||||
end note
|
||||
|
||||
state validacion <<choice>>
|
||||
|
||||
SinMaterias --> validacion : intenta inscribir
|
||||
Parcial --> validacion : intenta inscribir
|
||||
|
||||
validacion --> Parcial : [válido]
|
||||
validacion --> [*] : [inválido]\nmuestra error
|
||||
|
||||
@enduml
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -0,0 +1,67 @@
|
|||
@startuml deployment
|
||||
!theme plain
|
||||
skinparam nodeBackgroundColor #F8F9FA
|
||||
skinparam nodeBorderColor #495057
|
||||
skinparam componentBackgroundColor #E9ECEF
|
||||
|
||||
title Sistema de Registro de Estudiantes - Diagrama de Despliegue
|
||||
|
||||
node "Cliente" as client {
|
||||
component "Navegador Web" as browser {
|
||||
[Angular SPA]
|
||||
}
|
||||
}
|
||||
|
||||
node "Docker Host" as docker {
|
||||
|
||||
node "student-frontend" as frontendContainer <<container>> {
|
||||
component "Nginx" as nginx {
|
||||
[Static Files]
|
||||
[Reverse Proxy]
|
||||
}
|
||||
}
|
||||
|
||||
node "student-api" as apiContainer <<container>> {
|
||||
component "ASP.NET Core" as aspnet {
|
||||
[Kestrel Server]
|
||||
[GraphQL Endpoint]
|
||||
[Health Check]
|
||||
}
|
||||
}
|
||||
|
||||
node "student-db" as dbContainer <<container>> {
|
||||
database "SQL Server 2022" as sqlserver {
|
||||
[StudentEnrollment DB]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' Conexiones
|
||||
browser --> nginx : HTTP :80
|
||||
nginx --> aspnet : HTTP :5000\n/graphql
|
||||
aspnet --> sqlserver : TCP :1433
|
||||
|
||||
note right of nginx
|
||||
<b>Nginx Config:</b>
|
||||
- Gzip/Brotli compression
|
||||
- Static file caching
|
||||
- GraphQL proxy
|
||||
- Security headers
|
||||
end note
|
||||
|
||||
note right of aspnet
|
||||
<b>Optimizaciones:</b>
|
||||
- Server GC
|
||||
- ReadyToRun
|
||||
- Connection pooling
|
||||
- Rate limiting
|
||||
end note
|
||||
|
||||
note right of sqlserver
|
||||
<b>Recursos:</b>
|
||||
- 2 CPU cores
|
||||
- 2.5 GB RAM
|
||||
- Persistent volume
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
|
@ -0,0 +1,54 @@
|
|||
@startuml c4-context
|
||||
!theme plain
|
||||
skinparam rectangleBackgroundColor #F8F9FA
|
||||
skinparam rectangleBorderColor #495057
|
||||
|
||||
title Sistema de Registro de Estudiantes - Diagrama de Contexto (C4 Level 1)
|
||||
|
||||
skinparam rectangle {
|
||||
RoundCorner 10
|
||||
}
|
||||
|
||||
actor "Estudiante" as student <<Persona>>
|
||||
|
||||
rectangle "Sistema de Registro\nde Estudiantes" as system <<Software System>> #lightblue {
|
||||
}
|
||||
|
||||
rectangle "Base de Datos\nSQL Server" as database <<External System>> #lightgray {
|
||||
}
|
||||
|
||||
student --> system : Usa para registrarse\ne inscribirse en materias
|
||||
system --> database : Lee y escribe\ndatos de inscripciones
|
||||
|
||||
note right of student
|
||||
<b>Estudiante</b>
|
||||
Usuario del sistema que:
|
||||
- Se registra en el sistema
|
||||
- Se inscribe en materias (máx 3)
|
||||
- Ve sus compañeros de clase
|
||||
- Consulta inscripciones
|
||||
end note
|
||||
|
||||
note right of system
|
||||
<b>Sistema de Registro</b>
|
||||
Aplicación web que permite:
|
||||
- CRUD de estudiantes
|
||||
- Inscripción en materias
|
||||
- Validación de reglas de negocio
|
||||
- Visualización de compañeros
|
||||
|
||||
<b>Stack:</b>
|
||||
Frontend: Angular 21
|
||||
Backend: .NET 10 + GraphQL
|
||||
end note
|
||||
|
||||
note right of database
|
||||
<b>SQL Server 2022</b>
|
||||
Almacena:
|
||||
- Estudiantes
|
||||
- Profesores
|
||||
- Materias
|
||||
- Inscripciones
|
||||
end note
|
||||
|
||||
@enduml
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
|
@ -0,0 +1,45 @@
|
|||
# Diagramas UML - Sistema de Registro de Estudiantes
|
||||
|
||||
Diagramas de arquitectura y diseño del sistema, creados con PlantUML siguiendo el estándar UML 2.5.
|
||||
|
||||
## Diagramas Disponibles
|
||||
|
||||
| # | Diagrama | Archivo | Descripción |
|
||||
|---|----------|---------|-------------|
|
||||
| 1 | Casos de Uso | [01-use-cases](./01-use-cases.svg) | Funcionalidades del sistema desde perspectiva del estudiante |
|
||||
| 2 | Modelo de Dominio | [02-domain-model](./02-domain-model.svg) | Entidades, Value Objects y servicios de dominio |
|
||||
| 3 | Secuencia: Inscripción | [03-sequence-enrollment](./03-sequence-enrollment.svg) | Flujo completo del proceso de inscripción |
|
||||
| 4 | Componentes | [04-components](./04-components.svg) | Arquitectura de componentes frontend y backend |
|
||||
| 5 | Entidad-Relación | [05-entity-relationship](./05-entity-relationship.svg) | Modelo de base de datos |
|
||||
| 6 | Estados: Inscripción | [06-state-enrollment](./06-state-enrollment.svg) | Estados del estudiante según créditos |
|
||||
| 7 | Despliegue | [07-deployment](./07-deployment.svg) | Infraestructura Docker |
|
||||
| 8 | C4 Contexto | [08-c4-context](./08-c4-context.svg) | Vista de alto nivel del sistema |
|
||||
|
||||
## Regenerar Diagramas
|
||||
|
||||
Los archivos `.puml` son la fuente. Para regenerar los SVG:
|
||||
|
||||
```bash
|
||||
cd docs/architecture/diagrams
|
||||
|
||||
# Regenerar todos
|
||||
for f in *.puml; do
|
||||
cat "$f" | plantuml -tsvg -pipe > "${f%.puml}.svg"
|
||||
done
|
||||
|
||||
# O uno específico
|
||||
cat 01-use-cases.puml | plantuml -tsvg -pipe > 01-use-cases.svg
|
||||
```
|
||||
|
||||
## Requisitos
|
||||
|
||||
- [PlantUML](https://plantuml.com/) instalado
|
||||
- Java Runtime (requerido por PlantUML)
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt install plantuml
|
||||
|
||||
# Verificar instalación
|
||||
plantuml -version
|
||||
```
|
||||
|
|
@ -0,0 +1,386 @@
|
|||
# AN-003: Historias de Usuario
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes - Inter Rapidísimo
|
||||
**Rol:** Product Owner
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Épica Principal
|
||||
|
||||
**EP-001: Sistema de Inscripción de Estudiantes**
|
||||
|
||||
Como institución educativa, necesito un sistema web que permita a los estudiantes registrarse e inscribirse en materias siguiendo las reglas del programa de créditos, para gestionar eficientemente el proceso de matrícula.
|
||||
|
||||
---
|
||||
|
||||
## 2. Historias de Usuario
|
||||
|
||||
### US-001: Registro de Estudiante
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-001 |
|
||||
| **Épica** | EP-001 |
|
||||
| **Prioridad** | Alta |
|
||||
| **Story Points** | 5 |
|
||||
| **Sprint** | 1 |
|
||||
|
||||
**Historia:**
|
||||
> Como **estudiante nuevo**,
|
||||
> quiero **registrarme en el sistema con mi nombre y correo electrónico**,
|
||||
> para **poder acceder al programa de inscripción de materias**.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
|
||||
```gherkin
|
||||
Scenario: Registro exitoso de estudiante
|
||||
Given estoy en la página de registro
|
||||
When ingreso un nombre válido "Juan Pérez"
|
||||
And ingreso un email válido "juan@email.com"
|
||||
And presiono el botón "Registrar"
|
||||
Then el sistema crea mi cuenta
|
||||
And veo un mensaje de confirmación
|
||||
And soy redirigido a la página de inscripción
|
||||
|
||||
Scenario: Registro con email duplicado
|
||||
Given existe un estudiante con email "juan@email.com"
|
||||
When intento registrarme con el mismo email
|
||||
Then veo el mensaje "Ya existe un estudiante con este email"
|
||||
And el formulario no se envía
|
||||
|
||||
Scenario: Registro con email inválido
|
||||
Given estoy en la página de registro
|
||||
When ingreso un email inválido "juanemail.com"
|
||||
Then veo el mensaje "Formato de email no válido"
|
||||
And el botón de registro está deshabilitado
|
||||
```
|
||||
|
||||
**Notas Técnicas:**
|
||||
- Validación de email en frontend y backend
|
||||
- Email almacenado en minúsculas (normalización)
|
||||
|
||||
---
|
||||
|
||||
### US-002: Consulta de Materias Disponibles
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-002 |
|
||||
| **Épica** | EP-001 |
|
||||
| **Prioridad** | Alta |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | 1 |
|
||||
|
||||
**Historia:**
|
||||
> Como **estudiante registrado**,
|
||||
> quiero **ver el catálogo de las 10 materias disponibles**,
|
||||
> para **conocer mis opciones de inscripción**.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
|
||||
```gherkin
|
||||
Scenario: Ver catálogo completo
|
||||
Given estoy autenticado como estudiante
|
||||
When accedo a la sección de materias
|
||||
Then veo una lista de 10 materias
|
||||
And cada materia muestra nombre, créditos (3) y profesor
|
||||
|
||||
Scenario: Ver detalle de materia
|
||||
Given estoy viendo el catálogo de materias
|
||||
When selecciono "Matemáticas I"
|
||||
Then veo el nombre del profesor asignado
|
||||
And veo que equivale a 3 créditos
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### US-003: Inscripción en Materia
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-003 |
|
||||
| **Épica** | EP-001 |
|
||||
| **Prioridad** | Alta |
|
||||
| **Story Points** | 8 |
|
||||
| **Sprint** | 1 |
|
||||
|
||||
**Historia:**
|
||||
> Como **estudiante registrado**,
|
||||
> quiero **inscribirme en una materia disponible**,
|
||||
> para **acumular créditos en mi programa académico**.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
|
||||
```gherkin
|
||||
Scenario: Inscripción exitosa
|
||||
Given tengo menos de 3 materias inscritas
|
||||
And la materia "Física I" está disponible para mí
|
||||
When presiono "Inscribir" en "Física I"
|
||||
Then la materia se agrega a mis inscripciones
|
||||
And mis créditos aumentan en 3
|
||||
And veo mensaje de confirmación
|
||||
|
||||
Scenario: Intento inscripción con máximo alcanzado
|
||||
Given ya tengo 3 materias inscritas
|
||||
When intento inscribir otra materia
|
||||
Then veo el mensaje "Ya tienes el máximo de 3 materias"
|
||||
And el botón de inscripción está deshabilitado
|
||||
|
||||
Scenario: Intento inscripción con mismo profesor
|
||||
Given estoy inscrito en "Matemáticas I" (Profesor A)
|
||||
When intento inscribir "Matemáticas II" (Profesor A)
|
||||
Then veo el mensaje "Ya tienes una materia con este profesor"
|
||||
And la inscripción no se procesa
|
||||
```
|
||||
|
||||
**Notas Técnicas:**
|
||||
- Validaciones RN-003 y RN-005 en capa de dominio
|
||||
- Transacción atómica para inscripción
|
||||
|
||||
---
|
||||
|
||||
### US-004: Cancelación de Inscripción
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-004 |
|
||||
| **Épica** | EP-001 |
|
||||
| **Prioridad** | Media |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | 1 |
|
||||
|
||||
**Historia:**
|
||||
> Como **estudiante inscrito**,
|
||||
> quiero **cancelar la inscripción de una materia**,
|
||||
> para **liberar espacio y poder inscribir otra**.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
|
||||
```gherkin
|
||||
Scenario: Cancelación exitosa
|
||||
Given estoy inscrito en "Física I"
|
||||
When presiono "Cancelar inscripción" en esa materia
|
||||
And confirmo la acción
|
||||
Then la materia se elimina de mis inscripciones
|
||||
And mis créditos disminuyen en 3
|
||||
And la materia vuelve a estar disponible
|
||||
|
||||
Scenario: Cancelar con confirmación
|
||||
Given estoy inscrito en "Física I"
|
||||
When presiono "Cancelar inscripción"
|
||||
Then veo un diálogo de confirmación
|
||||
And puedo cancelar o confirmar la acción
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### US-005: Visualización de Materias No Disponibles
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-005 |
|
||||
| **Épica** | EP-001 |
|
||||
| **Prioridad** | Media |
|
||||
| **Story Points** | 3 |
|
||||
| **Sprint** | 2 |
|
||||
|
||||
**Historia:**
|
||||
> Como **estudiante registrado**,
|
||||
> quiero **ver claramente qué materias no puedo inscribir y por qué**,
|
||||
> para **tomar decisiones informadas sobre mi inscripción**.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
|
||||
```gherkin
|
||||
Scenario: Ver materias bloqueadas por profesor
|
||||
Given estoy inscrito en "Matemáticas I" (Profesor A)
|
||||
When veo el catálogo de materias
|
||||
Then "Matemáticas II" aparece deshabilitada
|
||||
And veo el tooltip "Ya tienes materia con Profesor A"
|
||||
|
||||
Scenario: Ver contador de materias restantes
|
||||
Given tengo 2 materias inscritas
|
||||
When veo mi panel de inscripción
|
||||
Then veo "2/3 materias inscritas"
|
||||
And veo "Puedes inscribir 1 materia más"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### US-006: Consulta de Otros Estudiantes
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-006 |
|
||||
| **Épica** | EP-001 |
|
||||
| **Prioridad** | Baja |
|
||||
| **Story Points** | 2 |
|
||||
| **Sprint** | 2 |
|
||||
|
||||
**Historia:**
|
||||
> Como **estudiante registrado**,
|
||||
> quiero **ver la lista de otros estudiantes en el sistema**,
|
||||
> para **conocer quiénes están en el programa**.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
|
||||
```gherkin
|
||||
Scenario: Ver lista de estudiantes
|
||||
Given estoy autenticado como estudiante
|
||||
When accedo a la sección de estudiantes
|
||||
Then veo una lista de todos los estudiantes registrados
|
||||
And cada estudiante muestra solo su nombre
|
||||
```
|
||||
|
||||
**Notas:**
|
||||
- Por privacidad, no se muestra email ni materias de otros estudiantes
|
||||
|
||||
---
|
||||
|
||||
### US-007: Ver Compañeros de Clase
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-007 |
|
||||
| **Épica** | EP-001 |
|
||||
| **Prioridad** | Media |
|
||||
| **Story Points** | 5 |
|
||||
| **Sprint** | 2 |
|
||||
|
||||
**Historia:**
|
||||
> Como **estudiante inscrito en materias**,
|
||||
> quiero **ver los nombres de mis compañeros en cada clase**,
|
||||
> para **conocer con quiénes compartiré el curso**.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
|
||||
```gherkin
|
||||
Scenario: Ver compañeros por materia
|
||||
Given estoy inscrito en "Física I"
|
||||
And otros estudiantes también están inscritos en "Física I"
|
||||
When accedo a "Mis compañeros de clase"
|
||||
Then veo "Física I" como sección
|
||||
And bajo ella veo la lista de nombres de compañeros
|
||||
|
||||
Scenario: Sin compañeros en materia
|
||||
Given estoy inscrito en "Redes I"
|
||||
And ningún otro estudiante está inscrito en "Redes I"
|
||||
When accedo a "Mis compañeros de clase"
|
||||
Then veo "Redes I" con mensaje "Sin compañeros aún"
|
||||
|
||||
Scenario: Privacidad de datos
|
||||
Given estoy viendo mis compañeros de clase
|
||||
Then solo veo los nombres
|
||||
And NO veo emails ni otras materias de mis compañeros
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### US-008: Actualización de Datos Personales
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-008 |
|
||||
| **Épica** | EP-001 |
|
||||
| **Prioridad** | Baja |
|
||||
| **Story Points** | 2 |
|
||||
| **Sprint** | 2 |
|
||||
|
||||
**Historia:**
|
||||
> Como **estudiante registrado**,
|
||||
> quiero **actualizar mi nombre o correo electrónico**,
|
||||
> para **mantener mis datos actualizados**.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
|
||||
```gherkin
|
||||
Scenario: Actualización exitosa de nombre
|
||||
Given estoy en mi perfil
|
||||
When cambio mi nombre a "Juan Carlos Pérez"
|
||||
And presiono "Guardar"
|
||||
Then mis datos se actualizan
|
||||
And veo mensaje de confirmación
|
||||
|
||||
Scenario: Cambio de email a uno existente
|
||||
Given existe otro estudiante con email "otro@email.com"
|
||||
When intento cambiar mi email a "otro@email.com"
|
||||
Then veo el mensaje "Este email ya está registrado"
|
||||
And el cambio no se procesa
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### US-009: Eliminación de Cuenta
|
||||
|
||||
| Campo | Valor |
|
||||
|-------|-------|
|
||||
| **ID** | US-009 |
|
||||
| **Épica** | EP-001 |
|
||||
| **Prioridad** | Baja |
|
||||
| **Story Points** | 2 |
|
||||
| **Sprint** | 2 |
|
||||
|
||||
**Historia:**
|
||||
> Como **estudiante registrado**,
|
||||
> quiero **eliminar mi cuenta del sistema**,
|
||||
> para **remover mis datos si ya no deseo participar**.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
|
||||
```gherkin
|
||||
Scenario: Eliminación con confirmación
|
||||
Given estoy en mi perfil
|
||||
When presiono "Eliminar cuenta"
|
||||
Then veo un diálogo de confirmación
|
||||
When confirmo la eliminación
|
||||
Then mi cuenta y todas mis inscripciones se eliminan
|
||||
And soy redirigido a la página de inicio
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Backlog Priorizado
|
||||
|
||||
| Prioridad | Historia | Story Points | Sprint |
|
||||
|-----------|----------|--------------|--------|
|
||||
| 1 | US-001: Registro de Estudiante | 5 | 1 |
|
||||
| 2 | US-002: Consulta de Materias | 3 | 1 |
|
||||
| 3 | US-003: Inscripción en Materia | 8 | 1 |
|
||||
| 4 | US-004: Cancelación de Inscripción | 3 | 1 |
|
||||
| 5 | US-005: Materias No Disponibles | 3 | 2 |
|
||||
| 6 | US-007: Ver Compañeros | 5 | 2 |
|
||||
| 7 | US-006: Consulta de Estudiantes | 2 | 2 |
|
||||
| 8 | US-008: Actualizar Datos | 2 | 2 |
|
||||
| 9 | US-009: Eliminar Cuenta | 2 | 2 |
|
||||
|
||||
**Total Story Points:** 33
|
||||
|
||||
---
|
||||
|
||||
## 4. Definición de Listo (DoR)
|
||||
|
||||
- [ ] Historia tiene criterios de aceptación claros
|
||||
- [ ] Dependencias identificadas
|
||||
- [ ] Estimación en story points
|
||||
- [ ] Diseño de UI aprobado (si aplica)
|
||||
- [ ] APIs definidas
|
||||
|
||||
## 5. Definición de Terminado (DoD)
|
||||
|
||||
- [ ] Código implementado y revisado
|
||||
- [ ] Tests unitarios (>80% coverage)
|
||||
- [ ] Tests de integración pasando
|
||||
- [ ] Documentación actualizada
|
||||
- [ ] Validaciones de negocio probadas
|
||||
- [ ] Sin deuda técnica crítica
|
||||
|
||||
---
|
||||
|
||||
## 6. Aprobación
|
||||
|
||||
| Rol | Nombre | Fecha | Firma |
|
||||
|-----|--------|-------|-------|
|
||||
| Product Owner | Sistema | 2026-01-07 | ✓ |
|
||||
| Scrum Master | Pendiente | - | - |
|
||||
| Tech Lead | Pendiente | - | - |
|
||||
|
|
@ -0,0 +1,304 @@
|
|||
# AN-002: Reglas de Negocio
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes - Inter Rapidísimo
|
||||
**Rol:** Analista de Sistemas
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Resumen
|
||||
|
||||
Este documento define las reglas de negocio que gobiernan el sistema de registro de estudiantes. Estas reglas son invariantes y deben ser validadas en la capa de dominio.
|
||||
|
||||
---
|
||||
|
||||
## 2. Reglas de Negocio
|
||||
|
||||
### RN-001: Estructura del Catálogo de Materias
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RN-001 |
|
||||
| **Nombre** | Catálogo Fijo de Materias |
|
||||
| **Descripción** | El sistema contiene exactamente 10 materias académicas |
|
||||
| **Tipo** | Restricción Estructural |
|
||||
| **Severidad** | Crítica |
|
||||
|
||||
**Datos Iniciales (Seed):**
|
||||
|
||||
| ID | Materia | Profesor Asignado |
|
||||
|----|---------|-------------------|
|
||||
| 1 | Matemáticas I | Profesor A |
|
||||
| 2 | Matemáticas II | Profesor A |
|
||||
| 3 | Física I | Profesor B |
|
||||
| 4 | Física II | Profesor B |
|
||||
| 5 | Programación I | Profesor C |
|
||||
| 6 | Programación II | Profesor C |
|
||||
| 7 | Base de Datos I | Profesor D |
|
||||
| 8 | Base de Datos II | Profesor D |
|
||||
| 9 | Redes I | Profesor E |
|
||||
| 10 | Redes II | Profesor E |
|
||||
|
||||
**Validación:** `COUNT(Subjects) == 10`
|
||||
|
||||
---
|
||||
|
||||
### RN-002: Valor Uniforme de Créditos
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RN-002 |
|
||||
| **Nombre** | Créditos por Materia |
|
||||
| **Descripción** | Cada materia equivale exactamente a 3 créditos |
|
||||
| **Tipo** | Restricción de Valor |
|
||||
| **Severidad** | Crítica |
|
||||
|
||||
**Fórmula:**
|
||||
```
|
||||
Créditos_Materia = 3 (constante)
|
||||
Créditos_Estudiante = COUNT(Inscripciones) × 3
|
||||
Créditos_Máximos = 9 (3 materias × 3 créditos)
|
||||
```
|
||||
|
||||
**Validación:** `Subject.Credits == 3`
|
||||
|
||||
---
|
||||
|
||||
### RN-003: Límite de Inscripciones por Estudiante
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RN-003 |
|
||||
| **Nombre** | Máximo 3 Materias |
|
||||
| **Descripción** | Un estudiante solo puede inscribirse en máximo 3 materias |
|
||||
| **Tipo** | Restricción de Cardinalidad |
|
||||
| **Severidad** | Crítica |
|
||||
|
||||
**Escenarios:**
|
||||
|
||||
| Materias Actuales | Acción | Resultado |
|
||||
|-------------------|--------|-----------|
|
||||
| 0-2 | Inscribir | ✓ Permitido |
|
||||
| 3 | Inscribir | ✗ Rechazado |
|
||||
| 1-3 | Cancelar | ✓ Permitido |
|
||||
|
||||
**Validación:** `Student.Enrollments.Count <= 3`
|
||||
|
||||
**Mensaje de Error:** "El estudiante ya tiene el máximo de 3 materias inscritas"
|
||||
|
||||
---
|
||||
|
||||
### RN-004: Asignación Profesores-Materias
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RN-004 |
|
||||
| **Nombre** | Distribución de Profesores |
|
||||
| **Descripción** | Existen 5 profesores, cada uno dicta exactamente 2 materias |
|
||||
| **Tipo** | Restricción Estructural |
|
||||
| **Severidad** | Crítica |
|
||||
|
||||
**Invariantes:**
|
||||
- `COUNT(Professors) == 5`
|
||||
- `∀ Professor: COUNT(Professor.Subjects) == 2`
|
||||
- `∀ Subject: Subject.Professor != NULL`
|
||||
|
||||
**Validación:**
|
||||
```csharp
|
||||
professors.All(p => p.Subjects.Count == 2)
|
||||
subjects.All(s => s.ProfessorId != null)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### RN-005: Restricción de Profesor Único (CRÍTICA)
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RN-005 |
|
||||
| **Nombre** | Prohibición de Mismo Profesor |
|
||||
| **Descripción** | Un estudiante NO puede inscribirse en dos materias dictadas por el mismo profesor |
|
||||
| **Tipo** | Restricción de Integridad |
|
||||
| **Severidad** | **CRÍTICA** |
|
||||
|
||||
**Lógica de Validación:**
|
||||
|
||||
```
|
||||
PARA inscribir(estudiante, nuevaMateria):
|
||||
profesorNuevaMateria = nuevaMateria.Profesor
|
||||
profesoresActuales = estudiante.Inscripciones.Select(i => i.Materia.Profesor)
|
||||
|
||||
SI profesorNuevaMateria EN profesoresActuales:
|
||||
RECHAZAR "Ya tienes una materia con este profesor"
|
||||
SINO:
|
||||
PERMITIR inscripción
|
||||
```
|
||||
|
||||
**Casos de Prueba:**
|
||||
|
||||
| Estudiante tiene | Intenta inscribir | Resultado |
|
||||
|------------------|-------------------|-----------|
|
||||
| Matemáticas I (Prof A) | Matemáticas II (Prof A) | ✗ Rechazado |
|
||||
| Matemáticas I (Prof A) | Física I (Prof B) | ✓ Permitido |
|
||||
| Física I (Prof B), Programación I (Prof C) | Redes I (Prof E) | ✓ Permitido |
|
||||
| Física I (Prof B), Programación I (Prof C) | Física II (Prof B) | ✗ Rechazado |
|
||||
|
||||
**Mensaje de Error:** "No puedes inscribir {materia} porque ya tienes una materia con {profesor}"
|
||||
|
||||
---
|
||||
|
||||
### RN-006: Unicidad de Email
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RN-006 |
|
||||
| **Nombre** | Email Único por Estudiante |
|
||||
| **Descripción** | Cada estudiante debe tener un email único en el sistema |
|
||||
| **Tipo** | Restricción de Unicidad |
|
||||
| **Severidad** | Alta |
|
||||
|
||||
**Validación:** `UNIQUE(Student.Email)`
|
||||
|
||||
**Mensaje de Error:** "Ya existe un estudiante registrado con este email"
|
||||
|
||||
---
|
||||
|
||||
### RN-007: Formato de Email Válido
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RN-007 |
|
||||
| **Nombre** | Validación de Email |
|
||||
| **Descripción** | El email debe cumplir formato estándar RFC 5322 |
|
||||
| **Tipo** | Restricción de Formato |
|
||||
| **Severidad** | Alta |
|
||||
|
||||
**Patrón:** `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`
|
||||
|
||||
**Mensaje de Error:** "El formato del email no es válido"
|
||||
|
||||
---
|
||||
|
||||
### RN-008: Visibilidad de Compañeros
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RN-008 |
|
||||
| **Nombre** | Privacidad de Datos |
|
||||
| **Descripción** | Un estudiante solo puede ver el NOMBRE de sus compañeros de clase |
|
||||
| **Tipo** | Restricción de Privacidad |
|
||||
| **Severidad** | Media |
|
||||
|
||||
**Datos Visibles:**
|
||||
- ✓ Nombre del compañero
|
||||
- ✗ Email del compañero
|
||||
- ✗ Otras materias del compañero
|
||||
|
||||
**Condición:** Solo aplica para materias donde ambos estudiantes están inscritos
|
||||
|
||||
---
|
||||
|
||||
## 3. Matriz de Validación por Capa
|
||||
|
||||
| Regla | Domain | Application | Adapter |
|
||||
|-------|--------|-------------|---------|
|
||||
| RN-001 | ✗ | ✗ | Seed Data |
|
||||
| RN-002 | ValueObject | ✗ | ✗ |
|
||||
| RN-003 | DomainService | ✗ | ✗ |
|
||||
| RN-004 | ✗ | ✗ | Seed Data |
|
||||
| RN-005 | **DomainService** | ✗ | ✗ |
|
||||
| RN-006 | ✗ | Validator | DB Constraint |
|
||||
| RN-007 | ValueObject | Validator | ✗ |
|
||||
| RN-008 | ✗ | Query Handler | ✗ |
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementación en Código
|
||||
|
||||
### RN-003 y RN-005 (Domain Service)
|
||||
|
||||
```csharp
|
||||
// Domain/Services/EnrollmentDomainService.cs
|
||||
public class EnrollmentDomainService
|
||||
{
|
||||
public Result ValidateEnrollment(Student student, Subject newSubject)
|
||||
{
|
||||
// RN-003: Máximo 3 materias
|
||||
if (student.Enrollments.Count >= 3)
|
||||
return Result.Failure("MAX_ENROLLMENTS_EXCEEDED");
|
||||
|
||||
// RN-005: No repetir profesor
|
||||
var existingProfessorIds = student.Enrollments
|
||||
.Select(e => e.Subject.ProfessorId)
|
||||
.ToHashSet();
|
||||
|
||||
if (existingProfessorIds.Contains(newSubject.ProfessorId))
|
||||
return Result.Failure("SAME_PROFESSOR_CONSTRAINT");
|
||||
|
||||
return Result.Success();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### RN-007 (Value Object)
|
||||
|
||||
```csharp
|
||||
// Domain/ValueObjects/Email.cs
|
||||
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_REQUIRED");
|
||||
|
||||
if (!EmailRegex.IsMatch(email))
|
||||
return Result.Failure<Email>("EMAIL_INVALID_FORMAT");
|
||||
|
||||
return Result.Success(new Email(email.ToLowerInvariant()));
|
||||
}
|
||||
|
||||
private static readonly Regex EmailRegex = new(
|
||||
@"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
RegexOptions.Compiled);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Diagrama de Restricciones
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ ESTUDIANTE │
|
||||
│ - Nombre │
|
||||
│ - Email (único) │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
│ máx 3 inscripciones
|
||||
│ (RN-003)
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ INSCRIPCIÓN │
|
||||
└──────────┬──────────┘
|
||||
│
|
||||
no puede repetir │ profesor (RN-005)
|
||||
▼
|
||||
┌─────────────┐ ┌─────────────────────┐ ┌─────────────┐
|
||||
│ PROFESOR │──────│ MATERIA │──────│ 3 CRÉDITOS │
|
||||
│ (5 total) │ 1:2 │ (10 total) │ │ (RN-002) │
|
||||
└─────────────┘ └─────────────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Aprobación
|
||||
|
||||
| Rol | Nombre | Fecha | Firma |
|
||||
|-----|--------|-------|-------|
|
||||
| Analista | Sistema | 2026-01-07 | ✓ |
|
||||
| Product Owner | Pendiente | - | - |
|
||||
| Arquitecto | Pendiente | - | - |
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
# AN-001: Análisis de Requisitos Funcionales
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes - Inter Rapidísimo
|
||||
**Rol:** Analista de Sistemas
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Resumen Ejecutivo
|
||||
|
||||
Sistema web para gestión de inscripciones estudiantiles con programa de créditos académicos. Permite CRUD de estudiantes, inscripción en materias con restricciones de negocio, y visualización de compañeros de clase.
|
||||
|
||||
---
|
||||
|
||||
## 2. Requisitos Funcionales Identificados
|
||||
|
||||
### RF-001: Registro de Estudiantes (CRUD)
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| **ID** | RF-001 |
|
||||
| **Nombre** | Gestión CRUD de Estudiantes |
|
||||
| **Descripción** | El sistema debe permitir crear, leer, actualizar y eliminar registros de estudiantes |
|
||||
| **Prioridad** | Alta |
|
||||
| **Fuente** | Enunciado punto 1 |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] CA-001.1: Usuario puede crear estudiante con nombre y email válido
|
||||
- [ ] CA-001.2: Usuario puede consultar lista de estudiantes registrados
|
||||
- [ ] CA-001.3: Usuario puede actualizar datos de un estudiante existente
|
||||
- [ ] CA-001.4: Usuario puede eliminar un estudiante (cascade con inscripciones)
|
||||
- [ ] CA-001.5: Email debe ser único en el sistema
|
||||
|
||||
---
|
||||
|
||||
### RF-002: Programa de Créditos
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| **ID** | RF-002 |
|
||||
| **Nombre** | Adhesión a Programa de Créditos |
|
||||
| **Descripción** | Los estudiantes se inscriben en un programa basado en créditos académicos |
|
||||
| **Prioridad** | Alta |
|
||||
| **Fuente** | Enunciado punto 2 |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] CA-002.1: Sistema muestra créditos totales del estudiante
|
||||
- [ ] CA-002.2: Créditos se calculan automáticamente según materias inscritas
|
||||
- [ ] CA-002.3: Máximo 9 créditos por estudiante (3 materias × 3 créditos)
|
||||
|
||||
---
|
||||
|
||||
### RF-003: Catálogo de Materias
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| **ID** | RF-003 |
|
||||
| **Nombre** | Gestión de 10 Materias |
|
||||
| **Descripción** | El sistema debe gestionar un catálogo fijo de 10 materias académicas |
|
||||
| **Prioridad** | Alta |
|
||||
| **Fuente** | Enunciado punto 3 |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] CA-003.1: Sistema contiene exactamente 10 materias predefinidas
|
||||
- [ ] CA-003.2: Usuario puede consultar catálogo completo de materias
|
||||
- [ ] CA-003.3: Cada materia muestra nombre, créditos y profesor asignado
|
||||
|
||||
---
|
||||
|
||||
### RF-004: Valor de Créditos por Materia
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| **ID** | RF-004 |
|
||||
| **Nombre** | Asignación de 3 Créditos por Materia |
|
||||
| **Descripción** | Cada materia del catálogo equivale a exactamente 3 créditos |
|
||||
| **Prioridad** | Alta |
|
||||
| **Fuente** | Enunciado punto 4 |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] CA-004.1: Todas las materias tienen valor fijo de 3 créditos
|
||||
- [ ] CA-004.2: Sistema muestra créditos en detalle de cada materia
|
||||
|
||||
---
|
||||
|
||||
### RF-005: Límite de Materias por Estudiante
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| **ID** | RF-005 |
|
||||
| **Nombre** | Máximo 3 Materias por Estudiante |
|
||||
| **Descripción** | Un estudiante solo puede inscribirse en máximo 3 materias |
|
||||
| **Prioridad** | Alta |
|
||||
| **Fuente** | Enunciado punto 5 |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] CA-005.1: Sistema impide inscripción si estudiante ya tiene 3 materias
|
||||
- [ ] CA-005.2: Sistema muestra contador de materias inscritas
|
||||
- [ ] CA-005.3: Mensaje de error claro al intentar exceder límite
|
||||
|
||||
---
|
||||
|
||||
### RF-006: Asignación Profesores-Materias
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| **ID** | RF-006 |
|
||||
| **Nombre** | 5 Profesores con 2 Materias c/u |
|
||||
| **Descripción** | El sistema tiene 5 profesores, cada uno dicta exactamente 2 materias |
|
||||
| **Prioridad** | Alta |
|
||||
| **Fuente** | Enunciado punto 6 |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] CA-006.1: Sistema contiene exactamente 5 profesores predefinidos
|
||||
- [ ] CA-006.2: Cada profesor tiene asignadas exactamente 2 materias
|
||||
- [ ] CA-006.3: Usuario puede ver qué profesor dicta cada materia
|
||||
|
||||
---
|
||||
|
||||
### RF-007: Restricción de Profesor Único
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| **ID** | RF-007 |
|
||||
| **Nombre** | Prohibición de Mismo Profesor |
|
||||
| **Descripción** | Un estudiante no puede inscribirse en dos materias del mismo profesor |
|
||||
| **Prioridad** | Crítica |
|
||||
| **Fuente** | Enunciado punto 7 |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] CA-007.1: Sistema valida profesor al momento de inscripción
|
||||
- [ ] CA-007.2: Materias no disponibles se muestran deshabilitadas con razón
|
||||
- [ ] CA-007.3: Mensaje de error específico: "Ya tienes una materia con este profesor"
|
||||
|
||||
---
|
||||
|
||||
### RF-008: Visualización de Registros
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| **ID** | RF-008 |
|
||||
| **Nombre** | Consulta de Otros Estudiantes |
|
||||
| **Descripción** | Cada estudiante puede ver en línea los registros de otros estudiantes |
|
||||
| **Prioridad** | Media |
|
||||
| **Fuente** | Enunciado punto 8 |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] CA-008.1: Usuario puede ver lista de todos los estudiantes registrados
|
||||
- [ ] CA-008.2: Información visible: nombre (según restricciones de privacidad)
|
||||
|
||||
---
|
||||
|
||||
### RF-009: Compañeros de Clase
|
||||
|
||||
| Atributo | Descripción |
|
||||
|----------|-------------|
|
||||
| **ID** | RF-009 |
|
||||
| **Nombre** | Ver Nombres de Compañeros por Materia |
|
||||
| **Descripción** | El estudiante puede ver solo el nombre de alumnos con quienes compartirá clase |
|
||||
| **Prioridad** | Media |
|
||||
| **Fuente** | Enunciado punto 9 |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] CA-009.1: Sistema muestra compañeros agrupados por materia
|
||||
- [ ] CA-009.2: Solo se muestra el nombre (no email ni otros datos)
|
||||
- [ ] CA-009.3: Solo muestra compañeros de materias donde está inscrito
|
||||
|
||||
---
|
||||
|
||||
## 3. Matriz de Trazabilidad
|
||||
|
||||
| Requisito | Historia | Componente Backend | Componente Frontend |
|
||||
|-----------|----------|-------------------|---------------------|
|
||||
| RF-001 | US-001 | StudentController, StudentService | StudentFormComponent |
|
||||
| RF-002 | US-002 | EnrollmentService | DashboardComponent |
|
||||
| RF-003 | US-003 | SubjectController | SubjectListComponent |
|
||||
| RF-004 | US-003 | Subject Entity | SubjectCardComponent |
|
||||
| RF-005 | US-004 | EnrollmentValidator | EnrollmentComponent |
|
||||
| RF-006 | US-003 | Professor Entity | ProfessorListComponent |
|
||||
| RF-007 | US-005 | EnrollmentDomainService | EnrollmentComponent |
|
||||
| RF-008 | US-006 | StudentQuery | StudentListComponent |
|
||||
| RF-009 | US-007 | ClassmatesQuery | ClassmatesComponent |
|
||||
|
||||
---
|
||||
|
||||
## 4. Dependencias entre Requisitos
|
||||
|
||||
```
|
||||
RF-001 (Estudiantes)
|
||||
↓
|
||||
RF-002 (Programa Créditos) ← RF-004 (3 créditos/materia)
|
||||
↓
|
||||
RF-005 (Max 3 materias) ← RF-003 (10 materias)
|
||||
↓ ↓
|
||||
RF-007 (Restricción profesor) ← RF-006 (5 profesores)
|
||||
↓
|
||||
RF-008/RF-009 (Visualización)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Aprobación
|
||||
|
||||
| Rol | Nombre | Fecha | Firma |
|
||||
|-----|--------|-------|-------|
|
||||
| Analista | Sistema | 2026-01-07 | ✓ |
|
||||
| Product Owner | Pendiente | - | - |
|
||||
| Líder Técnico | Pendiente | - | - |
|
||||
|
|
@ -0,0 +1,315 @@
|
|||
# AN-004: Requisitos No Funcionales
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes - Inter Rapidísimo
|
||||
**Rol:** Arquitecto de Software
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Resumen
|
||||
|
||||
Este documento define los atributos de calidad (requisitos no funcionales) que el sistema debe cumplir, estableciendo métricas medibles y criterios de aceptación.
|
||||
|
||||
---
|
||||
|
||||
## 2. Requisitos No Funcionales
|
||||
|
||||
### RNF-001: Rendimiento - Tiempo de Respuesta
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RNF-001 |
|
||||
| **Categoría** | Rendimiento |
|
||||
| **Prioridad** | Alta |
|
||||
| **Métrica** | Tiempo de respuesta < 200ms (P95) |
|
||||
|
||||
**Descripción:**
|
||||
El sistema debe responder a las solicitudes del usuario en menos de 200 milisegundos en el percentil 95.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
|
||||
| Operación | Tiempo Máximo |
|
||||
|-----------|---------------|
|
||||
| Consulta lista de estudiantes | < 150ms |
|
||||
| Consulta catálogo materias | < 100ms |
|
||||
| Inscripción en materia | < 200ms |
|
||||
| Crear/actualizar estudiante | < 200ms |
|
||||
| Consulta compañeros de clase | < 150ms |
|
||||
|
||||
**Medición:**
|
||||
- Herramienta: Application Insights / Logs estructurados
|
||||
- Ambiente: Producción simulada
|
||||
- Carga: 100 usuarios concurrentes
|
||||
|
||||
---
|
||||
|
||||
### RNF-002: Rendimiento - Throughput
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RNF-002 |
|
||||
| **Categoría** | Rendimiento |
|
||||
| **Prioridad** | Media |
|
||||
| **Métrica** | >= 500 requests/segundo |
|
||||
|
||||
**Descripción:**
|
||||
El sistema debe soportar al menos 500 solicitudes por segundo sin degradación.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] API soporta 500 req/s con latencia < 200ms
|
||||
- [ ] Sin errores 5xx bajo carga normal
|
||||
- [ ] CPU < 80% bajo carga máxima
|
||||
|
||||
---
|
||||
|
||||
### RNF-003: Seguridad - OWASP Top 10
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RNF-003 |
|
||||
| **Categoría** | Seguridad |
|
||||
| **Prioridad** | Crítica |
|
||||
| **Estándar** | OWASP Top 10 2021 |
|
||||
|
||||
**Descripción:**
|
||||
El sistema debe estar protegido contra las 10 vulnerabilidades más críticas según OWASP.
|
||||
|
||||
**Controles por Vulnerabilidad:**
|
||||
|
||||
| # | Vulnerabilidad | Control Implementado |
|
||||
|---|----------------|----------------------|
|
||||
| A01 | Broken Access Control | Validación de permisos en cada endpoint |
|
||||
| A02 | Cryptographic Failures | HTTPS obligatorio, hashing de datos sensibles |
|
||||
| A03 | Injection | Consultas parametrizadas (EF Core), validación de entrada |
|
||||
| A04 | Insecure Design | Clean Architecture, validación en dominio |
|
||||
| A05 | Security Misconfiguration | Headers de seguridad, CORS restrictivo |
|
||||
| A06 | Vulnerable Components | Auditoría de dependencias (`dotnet list package --vulnerable`) |
|
||||
| A07 | Auth Failures | Rate limiting, validación de sesión |
|
||||
| A08 | Data Integrity Failures | Validación de DTOs, firma de respuestas |
|
||||
| A09 | Logging Failures | Logs estructurados sin datos sensibles |
|
||||
| A10 | SSRF | Validación de URLs, no fetch de recursos externos |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] Análisis SAST sin vulnerabilidades críticas
|
||||
- [ ] Headers de seguridad configurados (CSP, X-Frame-Options, etc.)
|
||||
- [ ] Sin secretos en código fuente
|
||||
- [ ] HTTPS en todos los endpoints
|
||||
|
||||
---
|
||||
|
||||
### RNF-004: Seguridad - Validación de Entrada
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RNF-004 |
|
||||
| **Categoría** | Seguridad |
|
||||
| **Prioridad** | Alta |
|
||||
|
||||
**Descripción:**
|
||||
Toda entrada del usuario debe ser validada tanto en frontend como en backend.
|
||||
|
||||
**Reglas de Validación:**
|
||||
|
||||
| Campo | Regla Frontend | Regla Backend |
|
||||
|-------|----------------|---------------|
|
||||
| Nombre | Required, MaxLength(100) | FluentValidation |
|
||||
| Email | Required, EmailFormat | ValueObject + Validator |
|
||||
| IDs | Numeric | Tipo fuerte (int) |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] Ningún input llega a la BD sin validación
|
||||
- [ ] Mensajes de error claros y sin información sensible
|
||||
- [ ] Sanitización de HTML en campos de texto
|
||||
|
||||
---
|
||||
|
||||
### RNF-005: Usabilidad - Responsive Design
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RNF-005 |
|
||||
| **Categoría** | Usabilidad |
|
||||
| **Prioridad** | Alta |
|
||||
| **Estándar** | Mobile First |
|
||||
|
||||
**Descripción:**
|
||||
La interfaz debe adaptarse correctamente a diferentes tamaños de pantalla.
|
||||
|
||||
**Breakpoints Soportados:**
|
||||
|
||||
| Dispositivo | Ancho | Soporte |
|
||||
|-------------|-------|---------|
|
||||
| Mobile | 320px - 480px | Obligatorio |
|
||||
| Tablet | 481px - 768px | Obligatorio |
|
||||
| Desktop | 769px - 1024px | Obligatorio |
|
||||
| Large Desktop | > 1024px | Obligatorio |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] UI funcional en Chrome, Firefox, Safari, Edge
|
||||
- [ ] Sin scroll horizontal en mobile
|
||||
- [ ] Formularios usables con teclado virtual
|
||||
- [ ] Touch targets >= 44px
|
||||
|
||||
---
|
||||
|
||||
### RNF-006: Usabilidad - Accesibilidad
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RNF-006 |
|
||||
| **Categoría** | Usabilidad |
|
||||
| **Prioridad** | Media |
|
||||
| **Estándar** | WCAG 2.1 Nivel AA |
|
||||
|
||||
**Descripción:**
|
||||
El sistema debe ser accesible para usuarios con discapacidades.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] Contraste de colores >= 4.5:1
|
||||
- [ ] Navegación completa por teclado
|
||||
- [ ] Labels en todos los inputs
|
||||
- [ ] Atributos ARIA donde corresponda
|
||||
- [ ] Textos alternativos en imágenes
|
||||
|
||||
---
|
||||
|
||||
### RNF-007: Mantenibilidad - Cobertura de Tests
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RNF-007 |
|
||||
| **Categoría** | Mantenibilidad |
|
||||
| **Prioridad** | Alta |
|
||||
| **Métrica** | >= 80% cobertura |
|
||||
|
||||
**Descripción:**
|
||||
El código debe tener una cobertura de tests unitarios mínima del 80%.
|
||||
|
||||
**Distribución de Tests:**
|
||||
|
||||
| Capa | Cobertura Mínima | Tipo de Tests |
|
||||
|------|------------------|---------------|
|
||||
| Domain | 90% | Unit tests |
|
||||
| Application | 85% | Unit + Integration |
|
||||
| Adapters | 70% | Integration |
|
||||
| Frontend | 75% | Unit + Component |
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] Cobertura global >= 80%
|
||||
- [ ] Todas las reglas de negocio con tests
|
||||
- [ ] Tests ejecutables en CI/CD
|
||||
|
||||
---
|
||||
|
||||
### RNF-008: Mantenibilidad - Calidad de Código
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RNF-008 |
|
||||
| **Categoría** | Mantenibilidad |
|
||||
| **Prioridad** | Media |
|
||||
|
||||
**Descripción:**
|
||||
El código debe seguir estándares de calidad medibles.
|
||||
|
||||
**Métricas:**
|
||||
|
||||
| Métrica | Umbral |
|
||||
|---------|--------|
|
||||
| Complejidad ciclomática | < 10 por método |
|
||||
| Duplicación de código | < 3% |
|
||||
| Archivos | < 100 líneas |
|
||||
| Métodos | < 30 líneas |
|
||||
| Warnings del compilador | 0 |
|
||||
|
||||
**Herramientas:**
|
||||
- .NET: `dotnet format`, Roslyn Analyzers
|
||||
- Angular: ESLint, Prettier
|
||||
|
||||
---
|
||||
|
||||
### RNF-009: Disponibilidad
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RNF-009 |
|
||||
| **Categoría** | Disponibilidad |
|
||||
| **Prioridad** | Media |
|
||||
| **Métrica** | 99% uptime |
|
||||
|
||||
**Descripción:**
|
||||
El sistema debe estar disponible el 99% del tiempo durante horario laboral.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] Health checks implementados
|
||||
- [ ] Reinicio automático ante fallos
|
||||
- [ ] Graceful shutdown
|
||||
- [ ] Logs de errores para diagnóstico
|
||||
|
||||
---
|
||||
|
||||
### RNF-010: Escalabilidad
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RNF-010 |
|
||||
| **Categoría** | Escalabilidad |
|
||||
| **Prioridad** | Baja |
|
||||
|
||||
**Descripción:**
|
||||
La arquitectura debe permitir escalado horizontal sin cambios de código.
|
||||
|
||||
**Criterios de Aceptación:**
|
||||
- [ ] Aplicación stateless
|
||||
- [ ] Configuración externalizada
|
||||
- [ ] Base de datos separada de aplicación
|
||||
- [ ] Contenedores Docker funcionales
|
||||
|
||||
---
|
||||
|
||||
## 3. Matriz de Priorización
|
||||
|
||||
| ID | Requisito | Prioridad | Impacto | Esfuerzo |
|
||||
|----|-----------|-----------|---------|----------|
|
||||
| RNF-003 | Seguridad OWASP | Crítica | Alto | Alto |
|
||||
| RNF-001 | Tiempo respuesta | Alta | Alto | Medio |
|
||||
| RNF-004 | Validación entrada | Alta | Alto | Medio |
|
||||
| RNF-005 | Responsive | Alta | Alto | Medio |
|
||||
| RNF-007 | Cobertura tests | Alta | Medio | Alto |
|
||||
| RNF-002 | Throughput | Media | Medio | Medio |
|
||||
| RNF-006 | Accesibilidad | Media | Medio | Medio |
|
||||
| RNF-008 | Calidad código | Media | Medio | Bajo |
|
||||
| RNF-009 | Disponibilidad | Media | Alto | Medio |
|
||||
| RNF-010 | Escalabilidad | Baja | Bajo | Alto |
|
||||
|
||||
---
|
||||
|
||||
## 4. Estrategia de Validación
|
||||
|
||||
### Tests de Rendimiento
|
||||
```bash
|
||||
# k6 para load testing
|
||||
k6 run --vus 100 --duration 30s tests/performance/api-load.js
|
||||
```
|
||||
|
||||
### Tests de Seguridad
|
||||
```bash
|
||||
# OWASP ZAP scan
|
||||
docker run -t owasp/zap2docker-stable zap-baseline.py -t https://localhost:5001
|
||||
```
|
||||
|
||||
### Tests de Accesibilidad
|
||||
```bash
|
||||
# Lighthouse CI
|
||||
npx lighthouse https://localhost:4200 --only-categories=accessibility
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Aprobación
|
||||
|
||||
| Rol | Nombre | Fecha | Firma |
|
||||
|-----|--------|-------|-------|
|
||||
| Arquitecto | Sistema | 2026-01-07 | ✓ |
|
||||
| Líder Técnico | Pendiente | - | - |
|
||||
| QA Lead | Pendiente | - | - |
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
# AN-005: Análisis de Riesgos Técnicos
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes - Inter Rapidísimo
|
||||
**Rol:** Líder Técnico
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Resumen
|
||||
|
||||
Identificación y evaluación de riesgos técnicos que podrían impactar el desarrollo, calidad o entrega del sistema. Cada riesgo incluye probabilidad, impacto y estrategia de mitigación.
|
||||
|
||||
---
|
||||
|
||||
## 2. Matriz de Evaluación
|
||||
|
||||
| Probabilidad / Impacto | Bajo (1) | Medio (2) | Alto (3) |
|
||||
|------------------------|----------|-----------|----------|
|
||||
| **Alta (3)** | 3 | 6 | **9** |
|
||||
| **Media (2)** | 2 | 4 | 6 |
|
||||
| **Baja (1)** | 1 | 2 | 3 |
|
||||
|
||||
**Clasificación:**
|
||||
- **Crítico:** 6-9 (acción inmediata)
|
||||
- **Moderado:** 3-5 (plan de mitigación)
|
||||
- **Bajo:** 1-2 (monitorear)
|
||||
|
||||
---
|
||||
|
||||
## 3. Riesgos Identificados
|
||||
|
||||
### RT-001: Complejidad de Validaciones Cruzadas
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RT-001 |
|
||||
| **Categoría** | Desarrollo |
|
||||
| **Probabilidad** | Alta (3) |
|
||||
| **Impacto** | Alto (3) |
|
||||
| **Score** | **9 - Crítico** |
|
||||
|
||||
**Descripción:**
|
||||
Las validaciones de negocio (máximo 3 materias + no repetir profesor) requieren consultas cruzadas entre entidades. Una implementación incorrecta puede generar condiciones de carrera o validaciones inconsistentes.
|
||||
|
||||
**Escenarios de Riesgo:**
|
||||
1. Dos inscripciones concurrentes del mismo estudiante
|
||||
2. Validación en frontend que no se replica en backend
|
||||
3. Race condition al verificar profesor duplicado
|
||||
|
||||
**Estrategia de Mitigación:**
|
||||
|
||||
| Acción | Responsable | Plazo |
|
||||
|--------|-------------|-------|
|
||||
| Implementar validaciones en Domain Service (única fuente de verdad) | Dev Senior | Sprint 1 |
|
||||
| Usar transacciones con nivel de aislamiento Serializable | Dev Senior | Sprint 1 |
|
||||
| Tests de concurrencia con múltiples threads | QA | Sprint 1 |
|
||||
| Lock optimista con RowVersion en Enrollment | Dev | Sprint 1 |
|
||||
|
||||
**Código de Mitigación:**
|
||||
```csharp
|
||||
// Transacción con locking
|
||||
await using var transaction = await _context.Database
|
||||
.BeginTransactionAsync(IsolationLevel.Serializable);
|
||||
|
||||
var student = await _context.Students
|
||||
.Include(s => s.Enrollments)
|
||||
.ThenInclude(e => e.Subject)
|
||||
.FirstOrDefaultAsync(s => s.Id == command.StudentId);
|
||||
|
||||
// Validaciones en dominio
|
||||
_enrollmentService.ValidateEnrollment(student, subject);
|
||||
|
||||
await _context.SaveChangesAsync();
|
||||
await transaction.CommitAsync();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### RT-002: Integración Frontend-Backend (GraphQL)
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RT-002 |
|
||||
| **Categoría** | Integración |
|
||||
| **Probabilidad** | Media (2) |
|
||||
| **Impacto** | Alto (3) |
|
||||
| **Score** | **6 - Crítico** |
|
||||
|
||||
**Descripción:**
|
||||
GraphQL introduce complejidad adicional en la integración. Errores en el esquema, types incorrectos o problemas de N+1 queries pueden afectar rendimiento y desarrollo.
|
||||
|
||||
**Escenarios de Riesgo:**
|
||||
1. Mismatch entre schema GraphQL y DTOs
|
||||
2. N+1 queries sin DataLoaders
|
||||
3. Errores de tipos en Apollo Angular
|
||||
4. Over-fetching o under-fetching de datos
|
||||
|
||||
**Estrategia de Mitigación:**
|
||||
|
||||
| Acción | Responsable | Plazo |
|
||||
|--------|-------------|-------|
|
||||
| Generar tipos TypeScript desde schema (codegen) | Dev Frontend | Sprint 1 |
|
||||
| Implementar DataLoaders para todas las relaciones | Dev Backend | Sprint 1 |
|
||||
| Configurar GraphQL Voyager para visualizar schema | Dev | Sprint 1 |
|
||||
| Tests de integración GraphQL con Banana Cake Pop | QA | Sprint 1 |
|
||||
|
||||
**Configuración DataLoader:**
|
||||
```csharp
|
||||
// DataLoader para evitar N+1
|
||||
public class SubjectByIdDataLoader : BatchDataLoader<int, Subject>
|
||||
{
|
||||
protected override async Task<IReadOnlyDictionary<int, Subject>> LoadBatchAsync(
|
||||
IReadOnlyList<int> keys, CancellationToken ct)
|
||||
{
|
||||
return await _context.Subjects
|
||||
.Where(s => keys.Contains(s.Id))
|
||||
.ToDictionaryAsync(s => s.Id, ct);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### RT-003: Manejo de Concurrencia
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RT-003 |
|
||||
| **Categoría** | Arquitectura |
|
||||
| **Probabilidad** | Media (2) |
|
||||
| **Impacto** | Alto (3) |
|
||||
| **Score** | **6 - Crítico** |
|
||||
|
||||
**Descripción:**
|
||||
Múltiples usuarios podrían intentar inscribirse en la misma materia simultáneamente, generando conflictos o violaciones de reglas de negocio.
|
||||
|
||||
**Escenarios de Riesgo:**
|
||||
1. Dos estudiantes inscriben la última plaza disponible
|
||||
2. Estudiante inscribe 4ta materia por request concurrente
|
||||
3. Datos obsoletos en UI mientras otro usuario modifica
|
||||
|
||||
**Estrategia de Mitigación:**
|
||||
|
||||
| Acción | Responsable | Plazo |
|
||||
|--------|-------------|-------|
|
||||
| Implementar Optimistic Concurrency (RowVersion) | Dev Backend | Sprint 1 |
|
||||
| Revalidar en backend siempre (no confiar en frontend) | Dev Backend | Sprint 1 |
|
||||
| Mostrar toast de "datos actualizados" en conflictos | Dev Frontend | Sprint 2 |
|
||||
| Tests de stress con k6 | QA | Sprint 2 |
|
||||
|
||||
**Implementación:**
|
||||
```csharp
|
||||
// Entidad con concurrencia optimista
|
||||
public class Student
|
||||
{
|
||||
[Timestamp]
|
||||
public byte[] RowVersion { get; set; }
|
||||
}
|
||||
|
||||
// Manejo de conflicto
|
||||
catch (DbUpdateConcurrencyException)
|
||||
{
|
||||
return Result.Failure("CONCURRENT_MODIFICATION");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### RT-004: Rendimiento de Consultas Anidadas
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RT-004 |
|
||||
| **Categoría** | Rendimiento |
|
||||
| **Probabilidad** | Media (2) |
|
||||
| **Impacto** | Medio (2) |
|
||||
| **Score** | **4 - Moderado** |
|
||||
|
||||
**Descripción:**
|
||||
Consultas GraphQL anidadas (estudiantes → inscripciones → materias → profesores) pueden generar queries pesadas si no se optimizan.
|
||||
|
||||
**Escenarios de Riesgo:**
|
||||
1. Query de todos los estudiantes con todas sus relaciones
|
||||
2. Timeouts en consultas sin límite de profundidad
|
||||
3. Consumo excesivo de memoria en serialización
|
||||
|
||||
**Estrategia de Mitigación:**
|
||||
|
||||
| Acción | Responsable | Plazo |
|
||||
|--------|-------------|-------|
|
||||
| Configurar límite de profundidad en HotChocolate (max 5) | Dev Backend | Sprint 1 |
|
||||
| Implementar paginación en queries de listas | Dev Backend | Sprint 1 |
|
||||
| Indexar campos de búsqueda (Email, ProfessorId) | DBA | Sprint 1 |
|
||||
| Query complexity analyzer | Dev Backend | Sprint 2 |
|
||||
|
||||
**Configuración:**
|
||||
```csharp
|
||||
services.AddGraphQLServer()
|
||||
.SetMaxAllowedExecutionDepth(5)
|
||||
.SetPagingOptions(new PagingOptions
|
||||
{
|
||||
MaxPageSize = 50,
|
||||
DefaultPageSize = 20
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### RT-005: Curva de Aprendizaje del Stack
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RT-005 |
|
||||
| **Categoría** | Equipo |
|
||||
| **Probabilidad** | Alta (3) |
|
||||
| **Impacto** | Bajo (1) |
|
||||
| **Score** | **3 - Moderado** |
|
||||
|
||||
**Descripción:**
|
||||
El stack incluye tecnologías relativamente nuevas (HotChocolate, Angular Signals, Apollo Angular) que pueden requerir tiempo de aprendizaje.
|
||||
|
||||
**Tecnologías con Curva:**
|
||||
- HotChocolate (GraphQL .NET)
|
||||
- Mapster (alternativa a AutoMapper)
|
||||
- Angular Signals
|
||||
- Apollo Angular
|
||||
|
||||
**Estrategia de Mitigación:**
|
||||
|
||||
| Acción | Responsable | Plazo |
|
||||
|--------|-------------|-------|
|
||||
| Documentar patrones y ejemplos en wiki | Tech Lead | Sprint 1 |
|
||||
| Code reviews detallados en PRs | Senior Dev | Continuo |
|
||||
| Spike técnico para DataLoaders | Dev Backend | Sprint 1 |
|
||||
| Usar generador de código Apollo | Dev Frontend | Sprint 1 |
|
||||
|
||||
---
|
||||
|
||||
### RT-006: Seguridad en GraphQL
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RT-006 |
|
||||
| **Categoría** | Seguridad |
|
||||
| **Probabilidad** | Baja (1) |
|
||||
| **Impacto** | Alto (3) |
|
||||
| **Score** | **3 - Moderado** |
|
||||
|
||||
**Descripción:**
|
||||
GraphQL expone un endpoint flexible que puede ser abusado con queries maliciosas (deeply nested, introspection en prod, DoS).
|
||||
|
||||
**Escenarios de Riesgo:**
|
||||
1. Query extremadamente anidada consume recursos
|
||||
2. Introspection expone schema sensible
|
||||
3. Mutation masiva sin rate limiting
|
||||
|
||||
**Estrategia de Mitigación:**
|
||||
|
||||
| Acción | Responsable | Plazo |
|
||||
|--------|-------------|-------|
|
||||
| Deshabilitar introspection en producción | DevOps | Deploy |
|
||||
| Configurar query complexity limits | Dev Backend | Sprint 1 |
|
||||
| Rate limiting por IP (10 req/s) | DevOps | Sprint 2 |
|
||||
| Persisted queries en producción | Dev Backend | Sprint 3 |
|
||||
|
||||
**Configuración:**
|
||||
```csharp
|
||||
services.AddGraphQLServer()
|
||||
.AddMaxComplexityRule(100) // Límite de complejidad
|
||||
.AddMaxExecutionDepthRule(5)
|
||||
.ModifyOptions(o =>
|
||||
{
|
||||
// Deshabilitar introspection en prod
|
||||
if (!env.IsDevelopment())
|
||||
o.EnableSchemaIntrospection = false;
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### RT-007: Migración y Seeding de Datos
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RT-007 |
|
||||
| **Categoría** | Base de Datos |
|
||||
| **Probabilidad** | Baja (1) |
|
||||
| **Impacto** | Medio (2) |
|
||||
| **Score** | **2 - Bajo** |
|
||||
|
||||
**Descripción:**
|
||||
Los datos iniciales (5 profesores, 10 materias) deben estar correctamente relacionados. Errores en seeding pueden romper reglas de negocio.
|
||||
|
||||
**Escenarios de Riesgo:**
|
||||
1. Profesor asignado a más de 2 materias
|
||||
2. IDs inconsistentes entre migraciones
|
||||
3. Datos duplicados en re-ejecución de seed
|
||||
|
||||
**Estrategia de Mitigación:**
|
||||
|
||||
| Acción | Responsable | Plazo |
|
||||
|--------|-------------|-------|
|
||||
| Seeding idempotente (verificar existencia) | Dev Backend | Sprint 1 |
|
||||
| Validación post-seed en tests | QA | Sprint 1 |
|
||||
| Script de verificación de integridad | DBA | Sprint 1 |
|
||||
|
||||
---
|
||||
|
||||
### RT-008: Testing de Reglas de Negocio
|
||||
|
||||
| Atributo | Valor |
|
||||
|----------|-------|
|
||||
| **ID** | RT-008 |
|
||||
| **Categoría** | Calidad |
|
||||
| **Probabilidad** | Media (2) |
|
||||
| **Impacto** | Medio (2) |
|
||||
| **Score** | **4 - Moderado** |
|
||||
|
||||
**Descripción:**
|
||||
Las reglas de negocio complejas requieren cobertura exhaustiva de tests. Casos edge pueden quedar sin probar.
|
||||
|
||||
**Casos Edge Críticos:**
|
||||
1. Inscribir materia 3 cuando ya tiene 2 del mismo "tipo"
|
||||
2. Cancelar inscripción y re-inscribir rápidamente
|
||||
3. Estudiante con 3 materias intenta cambiar una
|
||||
|
||||
**Estrategia de Mitigación:**
|
||||
|
||||
| Acción | Responsable | Plazo |
|
||||
|--------|-------------|-------|
|
||||
| Property-based testing para combinaciones | QA | Sprint 1 |
|
||||
| Matriz de casos de prueba exhaustiva | QA | Sprint 1 |
|
||||
| Mutation testing (Stryker) | QA | Sprint 2 |
|
||||
|
||||
---
|
||||
|
||||
## 4. Matriz de Riesgos Consolidada
|
||||
|
||||
| ID | Riesgo | Prob | Impacto | Score | Prioridad |
|
||||
|----|--------|------|---------|-------|-----------|
|
||||
| RT-001 | Validaciones cruzadas | 3 | 3 | **9** | 1 |
|
||||
| RT-002 | Integración GraphQL | 2 | 3 | **6** | 2 |
|
||||
| RT-003 | Concurrencia | 2 | 3 | **6** | 3 |
|
||||
| RT-004 | Rendimiento queries | 2 | 2 | 4 | 4 |
|
||||
| RT-005 | Curva aprendizaje | 3 | 1 | 3 | 5 |
|
||||
| RT-006 | Seguridad GraphQL | 1 | 3 | 3 | 6 |
|
||||
| RT-008 | Testing reglas | 2 | 2 | 4 | 7 |
|
||||
| RT-007 | Seeding datos | 1 | 2 | 2 | 8 |
|
||||
|
||||
---
|
||||
|
||||
## 5. Plan de Contingencia
|
||||
|
||||
### Si RT-001 se materializa (validaciones fallan):
|
||||
1. Rollback a versión anterior
|
||||
2. Fix inmediato en Domain Service
|
||||
3. Agregar tests de regresión
|
||||
4. Re-deploy con verificación manual
|
||||
|
||||
### Si RT-002 se materializa (integración falla):
|
||||
1. Fallback a REST API simple
|
||||
2. Schema manual sin codegen
|
||||
3. Simplificar queries GraphQL
|
||||
|
||||
### Si RT-003 se materializa (race conditions):
|
||||
1. Lock pesimista temporal
|
||||
2. Queue de inscripciones
|
||||
3. Revisión manual de conflictos
|
||||
|
||||
---
|
||||
|
||||
## 6. Indicadores de Monitoreo
|
||||
|
||||
| Riesgo | Indicador | Umbral de Alerta |
|
||||
|--------|-----------|------------------|
|
||||
| RT-001 | Inscripciones fallidas por validación | > 5% |
|
||||
| RT-002 | Errores GraphQL 500 | > 1% |
|
||||
| RT-003 | DbUpdateConcurrencyException | > 10/hora |
|
||||
| RT-004 | Query time P95 | > 500ms |
|
||||
| RT-006 | Requests bloqueados por rate limit | > 100/hora |
|
||||
|
||||
---
|
||||
|
||||
## 7. Revisión de Riesgos
|
||||
|
||||
| Fecha | Revisor | Acción |
|
||||
|-------|---------|--------|
|
||||
| Sprint 1 Review | Tech Lead | Evaluar RT-001, RT-002 |
|
||||
| Sprint 2 Review | Tech Lead | Evaluar RT-003, RT-004 |
|
||||
| Pre-deploy | QA Lead | Validar mitigaciones |
|
||||
|
||||
---
|
||||
|
||||
## 8. Aprobación
|
||||
|
||||
| Rol | Nombre | Fecha | Firma |
|
||||
|-----|--------|-------|-------|
|
||||
| Líder Técnico | Sistema | 2026-01-07 | ✓ |
|
||||
| Arquitecto | Pendiente | - | - |
|
||||
| Project Manager | Pendiente | - | - |
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
# DI-001: Arquitectura Backend
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Patrón: Clean Architecture + CQRS
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ HOST │
|
||||
│ (Program.cs, DI, Configuration) │
|
||||
└─────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────────▼──────────────────────────────────┐
|
||||
│ ADAPTERS │
|
||||
│ ┌──────────────────────┬─────────────────────────────┐ │
|
||||
│ │ DRIVING (Primary) │ DRIVEN (Secondary) │ │
|
||||
│ │ ────────────────── │ ───────────────────── │ │
|
||||
│ │ GraphQL API │ Persistence (EF Core) │ │
|
||||
│ │ (HotChocolate) │ DataLoaders │ │
|
||||
│ └──────────────────────┴─────────────────────────────┘ │
|
||||
└─────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────────▼──────────────────────────────────┐
|
||||
│ APPLICATION │
|
||||
│ Commands / Queries / Handlers / Validators │
|
||||
└─────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────────▼──────────────────────────────────┐
|
||||
│ DOMAIN │
|
||||
│ Entities / Value Objects / Services / Ports │
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Regla de Dependencia
|
||||
|
||||
```
|
||||
Host → Adapters → Application → Domain
|
||||
↑
|
||||
Adapters implementa Ports
|
||||
```
|
||||
|
||||
**INVIOLABLE:** Domain NO depende de nada externo.
|
||||
|
||||
---
|
||||
|
||||
## 3. Responsabilidades por Capa
|
||||
|
||||
| Capa | Responsabilidad | Tecnología |
|
||||
|------|-----------------|------------|
|
||||
| **Domain** | Entidades, Value Objects, reglas de negocio puras, Ports (interfaces) | C# puro |
|
||||
| **Application** | Casos de uso (Commands/Queries), DTOs, Validators, orquestación | MediatR, FluentValidation, Mapster |
|
||||
| **Adapters/Driving** | GraphQL API (Types, Resolvers, Mutations) | HotChocolate |
|
||||
| **Adapters/Driven** | Repositorios, DbContext, DataLoaders | EF Core, SQL Server |
|
||||
| **Host** | Composition Root, DI, Middleware | ASP.NET Core |
|
||||
|
||||
---
|
||||
|
||||
## 4. Estructura de Proyectos
|
||||
|
||||
```
|
||||
src/backend/
|
||||
├── Domain/
|
||||
│ ├── Entities/ # Student, Subject, Professor, Enrollment
|
||||
│ ├── ValueObjects/ # Email, Credits
|
||||
│ ├── Services/ # EnrollmentDomainService
|
||||
│ ├── Ports/
|
||||
│ │ └── Repositories/ # IStudentRepository, ISubjectRepository
|
||||
│ └── Exceptions/ # MaxEnrollmentsException, SameProfessorException
|
||||
│
|
||||
├── Application/
|
||||
│ ├── Students/
|
||||
│ │ ├── Commands/ # CreateStudentCommand, UpdateStudentCommand
|
||||
│ │ ├── Queries/ # GetStudentsQuery, GetStudentByIdQuery
|
||||
│ │ └── DTOs/ # StudentDto, CreateStudentInput
|
||||
│ ├── Enrollments/
|
||||
│ │ ├── Commands/ # EnrollCommand, UnenrollCommand
|
||||
│ │ ├── Queries/ # GetClassmatesQuery
|
||||
│ │ └── DTOs/ # EnrollmentDto, EnrollInput
|
||||
│ └── Common/
|
||||
│ ├── Behaviors/ # ValidationBehavior
|
||||
│ └── Mappings/ # MapsterConfig
|
||||
│
|
||||
├── Adapters/
|
||||
│ ├── Driving/
|
||||
│ │ └── Api/
|
||||
│ │ ├── Types/ # StudentType, SubjectType
|
||||
│ │ ├── Queries/ # QueryResolvers
|
||||
│ │ └── Mutations/ # MutationResolvers
|
||||
│ └── Driven/
|
||||
│ └── Persistence/
|
||||
│ ├── Context/ # AppDbContext
|
||||
│ ├── Configs/ # StudentConfiguration
|
||||
│ ├── Repos/ # StudentRepository
|
||||
│ └── Seeding/ # DataSeeder
|
||||
│
|
||||
└── Host/
|
||||
└── Program.cs # Entry point + DI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Flujo de una Request
|
||||
|
||||
```
|
||||
1. GraphQL Request → HotChocolate Resolver
|
||||
2. Resolver → Application Handler (via MediatR)
|
||||
3. Handler → Validator (FluentValidation)
|
||||
4. Handler → Domain Service (reglas de negocio)
|
||||
5. Handler → Repository (persistencia)
|
||||
6. Repository → DbContext → SQL Server
|
||||
7. Response ← DTO ← Mapper ← Entity
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Decisiones de Arquitectura
|
||||
|
||||
| Decisión | Razón |
|
||||
|----------|-------|
|
||||
| GraphQL vs REST | Flexibilidad en queries, evita over/under-fetching |
|
||||
| HotChocolate | Mejor soporte .NET, Code-First, DataLoaders |
|
||||
| MediatR | Desacopla handlers, facilita CQRS |
|
||||
| Mapster | Más rápido que AutoMapper |
|
||||
| FluentValidation | Validaciones declarativas y testeables |
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
# DI-005: Arquitectura Frontend
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Stack Tecnológico
|
||||
|
||||
| Tecnología | Propósito |
|
||||
|------------|-----------|
|
||||
| Angular 21 | Framework SPA |
|
||||
| Angular Material | UI Components |
|
||||
| Apollo Angular | Cliente GraphQL |
|
||||
| Signals | Estado reactivo |
|
||||
| TypeScript | Tipado estático |
|
||||
|
||||
---
|
||||
|
||||
## 2. Estructura de Carpetas
|
||||
|
||||
```
|
||||
src/frontend/src/app/
|
||||
├── core/ # Singleton services
|
||||
│ ├── services/
|
||||
│ │ ├── student.service.ts
|
||||
│ │ └── enrollment.service.ts
|
||||
│ ├── graphql/
|
||||
│ │ ├── generated/ # Tipos generados
|
||||
│ │ ├── queries/
|
||||
│ │ │ ├── students.graphql
|
||||
│ │ │ └── subjects.graphql
|
||||
│ │ └── mutations/
|
||||
│ │ ├── student.graphql
|
||||
│ │ └── enrollment.graphql
|
||||
│ └── interceptors/
|
||||
│ └── error.interceptor.ts
|
||||
│
|
||||
├── shared/ # Reutilizables
|
||||
│ ├── components/
|
||||
│ │ ├── confirm-dialog/
|
||||
│ │ └── loading-spinner/
|
||||
│ └── pipes/
|
||||
│ └── credits.pipe.ts
|
||||
│
|
||||
└── features/ # Módulos por funcionalidad
|
||||
├── students/
|
||||
│ ├── pages/
|
||||
│ │ ├── student-list/
|
||||
│ │ └── student-form/
|
||||
│ └── components/
|
||||
│ └── student-card/
|
||||
│
|
||||
├── enrollment/
|
||||
│ ├── pages/
|
||||
│ │ └── enrollment-page/
|
||||
│ └── components/
|
||||
│ ├── subject-selector/
|
||||
│ └── enrolled-subjects/
|
||||
│
|
||||
└── classmates/
|
||||
└── pages/
|
||||
└── classmates-page/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Configuración Apollo
|
||||
|
||||
```typescript
|
||||
// app.config.ts
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideApollo(() => ({
|
||||
link: httpLink.create({ uri: environment.graphqlUrl }),
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
watchQuery: { fetchPolicy: 'cache-and-network' },
|
||||
mutate: { errorPolicy: 'all' }
|
||||
}
|
||||
}))
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Gestión de Estado
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Apollo Cache │
|
||||
│ (Estado del servidor: students, subjects) │
|
||||
└─────────────────────────────────────────────┘
|
||||
▲
|
||||
│ watchQuery / mutate
|
||||
▼
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Services │
|
||||
│ (Encapsulan operaciones GraphQL) │
|
||||
└─────────────────────────────────────────────┘
|
||||
▲
|
||||
│ inject()
|
||||
▼
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ Components │
|
||||
│ (Signals para estado local/UI) │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Ejemplo de Service
|
||||
|
||||
```typescript
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class StudentService {
|
||||
private apollo = inject(Apollo);
|
||||
|
||||
getStudents() {
|
||||
return this.apollo.watchQuery<{ students: Student[] }>({
|
||||
query: GET_STUDENTS,
|
||||
fetchPolicy: 'cache-and-network'
|
||||
}).valueChanges;
|
||||
}
|
||||
|
||||
createStudent(input: CreateStudentInput) {
|
||||
return this.apollo.mutate<{ createStudent: StudentPayload }>({
|
||||
mutation: CREATE_STUDENT,
|
||||
variables: { input },
|
||||
refetchQueries: [{ query: GET_STUDENTS }]
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Ejemplo de Componente (Signals)
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'app-student-list',
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
@if (loading()) {
|
||||
<mat-spinner />
|
||||
} @else {
|
||||
<mat-table [dataSource]="students()">
|
||||
<!-- columns -->
|
||||
</mat-table>
|
||||
}
|
||||
`
|
||||
})
|
||||
export class StudentListComponent {
|
||||
private studentService = inject(StudentService);
|
||||
|
||||
students = signal<Student[]>([]);
|
||||
loading = signal(true);
|
||||
|
||||
constructor() {
|
||||
this.studentService.getStudents().subscribe(({ data, loading }) => {
|
||||
this.students.set(data?.students ?? []);
|
||||
this.loading.set(loading);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Rutas (Lazy Loading)
|
||||
|
||||
```typescript
|
||||
export const routes: Routes = [
|
||||
{ path: '', redirectTo: 'students', pathMatch: 'full' },
|
||||
{
|
||||
path: 'students',
|
||||
loadComponent: () => import('./features/students/pages/student-list/student-list.component')
|
||||
},
|
||||
{
|
||||
path: 'students/new',
|
||||
loadComponent: () => import('./features/students/pages/student-form/student-form.component')
|
||||
},
|
||||
{
|
||||
path: 'enrollment/:studentId',
|
||||
loadComponent: () => import('./features/enrollment/pages/enrollment-page/enrollment-page.component')
|
||||
},
|
||||
{
|
||||
path: 'classmates/:studentId',
|
||||
loadComponent: () => import('./features/classmates/pages/classmates-page/classmates-page.component')
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Decisiones de Diseño
|
||||
|
||||
| Decisión | Razón |
|
||||
|----------|-------|
|
||||
| Standalone Components | Angular moderno, tree-shakeable |
|
||||
| Signals vs RxJS | Más simple para estado local, menos boilerplate |
|
||||
| Apollo Cache | Single source of truth para datos del servidor |
|
||||
| OnPush | Mejor rendimiento, menos re-renders |
|
||||
| Lazy Loading | Bundle inicial pequeño |
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
# 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).
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
# DI-003: Diseño de Base de Datos
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Modelo Entidad-Relación
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PROFESSORS │
|
||||
├──────────────┬────────────────┬─────────────────────────────────┤
|
||||
│ Id │ INT │ PK, IDENTITY │
|
||||
│ Name │ NVARCHAR(100) │ NOT NULL │
|
||||
└──────────────┴────────────────┴─────────────────────────────────┘
|
||||
│
|
||||
│ 1:N (cada profesor → 2 materias)
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ SUBJECTS │
|
||||
├──────────────┬────────────────┬─────────────────────────────────┤
|
||||
│ Id │ INT │ PK, IDENTITY │
|
||||
│ Name │ NVARCHAR(100) │ NOT NULL │
|
||||
│ Credits │ INT │ DEFAULT 3, CHECK (Credits = 3) │
|
||||
│ ProfessorId │ INT │ FK → Professors.Id │
|
||||
└──────────────┴────────────────┴─────────────────────────────────┘
|
||||
│
|
||||
│ 1:N
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ENROLLMENTS │
|
||||
├──────────────┬────────────────┬─────────────────────────────────┤
|
||||
│ Id │ INT │ PK, IDENTITY │
|
||||
│ StudentId │ INT │ FK → Students.Id │
|
||||
│ SubjectId │ INT │ FK → Subjects.Id │
|
||||
│ EnrolledAt │ DATETIME2 │ DEFAULT GETUTCDATE() │
|
||||
└──────────────┴────────────────┴─────────────────────────────────┘
|
||||
▲
|
||||
│ 0..3:N
|
||||
│
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ STUDENTS │
|
||||
├──────────────┬────────────────┬─────────────────────────────────┤
|
||||
│ Id │ INT │ PK, IDENTITY │
|
||||
│ Name │ NVARCHAR(100) │ NOT NULL │
|
||||
│ Email │ NVARCHAR(255) │ NOT NULL, UNIQUE │
|
||||
│ RowVersion │ ROWVERSION │ Concurrency control │
|
||||
└──────────────┴────────────────┴─────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Script DDL
|
||||
|
||||
```sql
|
||||
-- Crear base de datos
|
||||
CREATE DATABASE StudentEnrollment;
|
||||
GO
|
||||
|
||||
USE StudentEnrollment;
|
||||
GO
|
||||
|
||||
-- Tabla: Professors
|
||||
CREATE TABLE Professors (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
Name NVARCHAR(100) NOT NULL
|
||||
);
|
||||
|
||||
-- Tabla: Subjects
|
||||
CREATE TABLE Subjects (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
Name NVARCHAR(100) NOT NULL,
|
||||
Credits INT NOT NULL DEFAULT 3 CHECK (Credits = 3),
|
||||
ProfessorId INT NOT NULL,
|
||||
CONSTRAINT FK_Subjects_Professors
|
||||
FOREIGN KEY (ProfessorId) REFERENCES Professors(Id)
|
||||
);
|
||||
|
||||
-- Tabla: Students
|
||||
CREATE TABLE Students (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
Name NVARCHAR(100) NOT NULL,
|
||||
Email NVARCHAR(255) NOT NULL,
|
||||
RowVersion ROWVERSION,
|
||||
CONSTRAINT UQ_Students_Email UNIQUE (Email)
|
||||
);
|
||||
|
||||
-- Tabla: Enrollments
|
||||
CREATE TABLE Enrollments (
|
||||
Id INT IDENTITY(1,1) PRIMARY KEY,
|
||||
StudentId INT NOT NULL,
|
||||
SubjectId INT NOT NULL,
|
||||
EnrolledAt DATETIME2 NOT NULL DEFAULT GETUTCDATE(),
|
||||
CONSTRAINT FK_Enrollments_Students
|
||||
FOREIGN KEY (StudentId) REFERENCES Students(Id) ON DELETE CASCADE,
|
||||
CONSTRAINT FK_Enrollments_Subjects
|
||||
FOREIGN KEY (SubjectId) REFERENCES Subjects(Id),
|
||||
CONSTRAINT UQ_Enrollments_Student_Subject
|
||||
UNIQUE (StudentId, SubjectId)
|
||||
);
|
||||
|
||||
-- Índices
|
||||
CREATE INDEX IX_Subjects_ProfessorId ON Subjects(ProfessorId);
|
||||
CREATE INDEX IX_Enrollments_StudentId ON Enrollments(StudentId);
|
||||
CREATE INDEX IX_Enrollments_SubjectId ON Enrollments(SubjectId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Datos Iniciales (Seed)
|
||||
|
||||
```sql
|
||||
-- 5 Profesores
|
||||
INSERT INTO Professors (Name) VALUES
|
||||
('Dr. García'), -- Id: 1
|
||||
('Dra. Martínez'), -- Id: 2
|
||||
('Dr. López'), -- Id: 3
|
||||
('Dra. Rodríguez'), -- Id: 4
|
||||
('Dr. Hernández'); -- Id: 5
|
||||
|
||||
-- 10 Materias (2 por profesor)
|
||||
INSERT INTO Subjects (Name, Credits, ProfessorId) VALUES
|
||||
('Matemáticas I', 3, 1),
|
||||
('Matemáticas II', 3, 1),
|
||||
('Física I', 3, 2),
|
||||
('Física II', 3, 2),
|
||||
('Programación I', 3, 3),
|
||||
('Programación II', 3, 3),
|
||||
('Base de Datos I', 3, 4),
|
||||
('Base de Datos II', 3, 4),
|
||||
('Redes I', 3, 5),
|
||||
('Redes II', 3, 5);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Constraints de Negocio
|
||||
|
||||
| Constraint | Tipo | Descripción |
|
||||
|------------|------|-------------|
|
||||
| `UQ_Students_Email` | UNIQUE | Email único por estudiante |
|
||||
| `UQ_Enrollments_Student_Subject` | UNIQUE | No duplicar inscripción |
|
||||
| `CHECK (Credits = 3)` | CHECK | Créditos siempre = 3 |
|
||||
| `ON DELETE CASCADE` | FK | Eliminar inscripciones al eliminar estudiante |
|
||||
|
||||
---
|
||||
|
||||
## 5. Vistas Útiles
|
||||
|
||||
```sql
|
||||
-- Vista: Compañeros de clase
|
||||
CREATE VIEW vw_Classmates AS
|
||||
SELECT
|
||||
e1.StudentId,
|
||||
s.Name AS SubjectName,
|
||||
st.Name AS ClassmateName
|
||||
FROM Enrollments e1
|
||||
INNER JOIN Enrollments e2 ON e1.SubjectId = e2.SubjectId
|
||||
AND e1.StudentId != e2.StudentId
|
||||
INNER JOIN Subjects s ON e1.SubjectId = s.Id
|
||||
INNER JOIN Students st ON e2.StudentId = st.Id;
|
||||
|
||||
-- Vista: Materias disponibles por estudiante
|
||||
CREATE VIEW vw_AvailableSubjects AS
|
||||
SELECT
|
||||
st.Id AS StudentId,
|
||||
su.Id AS SubjectId,
|
||||
su.Name AS SubjectName,
|
||||
p.Name AS ProfessorName,
|
||||
CASE
|
||||
WHEN e.Id IS NOT NULL THEN 0 -- Ya inscrito
|
||||
WHEN ep.ProfessorId IS NOT NULL THEN 0 -- Mismo profesor
|
||||
ELSE 1
|
||||
END AS IsAvailable
|
||||
FROM Students st
|
||||
CROSS JOIN Subjects su
|
||||
INNER JOIN Professors p ON su.ProfessorId = p.Id
|
||||
LEFT JOIN Enrollments e ON st.Id = e.StudentId AND su.Id = e.SubjectId
|
||||
LEFT JOIN (
|
||||
SELECT DISTINCT e.StudentId, s.ProfessorId
|
||||
FROM Enrollments e
|
||||
INNER JOIN Subjects s ON e.SubjectId = s.Id
|
||||
) ep ON st.Id = ep.StudentId AND su.ProfessorId = ep.ProfessorId;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Diccionario de Datos
|
||||
|
||||
| Tabla | Campo | Tipo | Descripción |
|
||||
|-------|-------|------|-------------|
|
||||
| Students | Id | INT | Identificador único |
|
||||
| Students | Name | NVARCHAR(100) | Nombre completo |
|
||||
| Students | Email | NVARCHAR(255) | Correo (único) |
|
||||
| Students | RowVersion | ROWVERSION | Control de concurrencia |
|
||||
| Subjects | Credits | INT | Siempre 3 |
|
||||
| Enrollments | EnrolledAt | DATETIME2 | Fecha de inscripción UTC |
|
||||
|
|
@ -0,0 +1,166 @@
|
|||
# DI-006: Diseño de Componentes UI
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Mapa de Pantallas
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ APP LAYOUT │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ [Logo] Sistema de Estudiantes [Estudiantes] [Mat] │
|
||||
├──────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ <router-outlet> │
|
||||
│ │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
Rutas:
|
||||
├── /students → Lista de estudiantes
|
||||
├── /students/new → Formulario crear
|
||||
├── /students/:id/edit → Formulario editar
|
||||
├── /enrollment/:id → Inscripción de materias
|
||||
└── /classmates/:id → Compañeros de clase
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Wireframes
|
||||
|
||||
### 2.1 Lista de Estudiantes (`/students`)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ESTUDIANTES [+ Nuevo Estudiante] │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 🔍 Buscar estudiante... │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ NOMBRE │ EMAIL │ CRÉDITOS │ ACCIONES│ │
|
||||
│ ├───────────────────────────────────────────────────────┤ │
|
||||
│ │ Juan Pérez │ juan@email.com │ 6/9 │ ✏️ 🗑️ 📚 │ │
|
||||
│ │ María García │ maria@email.com │ 9/9 │ ✏️ 🗑️ 📚 │ │
|
||||
│ │ Carlos López │ carlos@mail.com │ 3/9 │ ✏️ 🗑️ 📚 │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [← Anterior] [Siguiente →] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
Leyenda: ✏️ Editar 🗑️ Eliminar 📚 Inscripción
|
||||
```
|
||||
|
||||
### 2.2 Formulario Estudiante (`/students/new`)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ← Volver NUEVO ESTUDIANTE │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Nombre * │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Juan Pérez │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ Email * │ │
|
||||
│ │ ┌─────────────────────────────────────────────────┐ │ │
|
||||
│ │ │ juan@email.com │ │ │
|
||||
│ │ └─────────────────────────────────────────────────┘ │ │
|
||||
│ │ ⚠️ Este email ya está registrado (error ejemplo) │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Cancelar] [Guardar] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.3 Inscripción de Materias (`/enrollment/:id`)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ← Volver INSCRIPCIÓN: Juan Pérez │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Créditos: ████████░░ 6/9 Materias: 2/3 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ MIS MATERIAS INSCRITAS │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ ✓ Física I │ Dra. Martínez │ [Cancelar] │ │
|
||||
│ │ ✓ Programación I │ Dr. López │ [Cancelar] │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ MATERIAS DISPONIBLES │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ ○ Matemáticas I │ Dr. García │ [Inscribir] │ │
|
||||
│ │ ○ Matemáticas II │ Dr. García │ [Inscribir] │ │
|
||||
│ │ ○ Física II │ Dra. Martínez │ ⚠️ Mismo prof │ │
|
||||
│ │ ○ Programación II │ Dr. López │ ⚠️ Mismo prof │ │
|
||||
│ │ ○ Base de Datos I │ Dra. Rodríguez │ [Inscribir] │ │
|
||||
│ │ ... │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ [Ver Compañeros de Clase] │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.4 Compañeros de Clase (`/classmates/:id`)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ← Volver MIS COMPAÑEROS: Juan Pérez │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 📚 FÍSICA I (Dra. Martínez) │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ • María García │ │
|
||||
│ │ • Carlos López │ │
|
||||
│ │ • Ana Rodríguez │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ 💻 PROGRAMACIÓN I (Dr. López) │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ • Pedro Sánchez │ │
|
||||
│ │ • Laura Torres │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Componentes Reutilizables
|
||||
|
||||
| Componente | Descripción | Props |
|
||||
|------------|-------------|-------|
|
||||
| `ConfirmDialog` | Modal de confirmación | title, message, onConfirm |
|
||||
| `LoadingSpinner` | Indicador de carga | size, color |
|
||||
| `ErrorAlert` | Mensaje de error | message, dismissible |
|
||||
| `CreditsBadge` | Badge de créditos | current, max |
|
||||
| `SubjectCard` | Card de materia | subject, actions |
|
||||
|
||||
---
|
||||
|
||||
## 4. Estados de UI
|
||||
|
||||
| Estado | Visual |
|
||||
|--------|--------|
|
||||
| Loading | Skeleton / Spinner |
|
||||
| Empty | Ilustración + mensaje |
|
||||
| Error | Alert rojo + retry |
|
||||
| Success | Toast verde |
|
||||
|
||||
---
|
||||
|
||||
## 5. Responsive Breakpoints
|
||||
|
||||
| Breakpoint | Comportamiento |
|
||||
|------------|----------------|
|
||||
| Mobile (<600px) | Cards apiladas, menú hamburguesa |
|
||||
| Tablet (600-960px) | Grid 2 columnas |
|
||||
| Desktop (>960px) | Tabla completa, sidebar |
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
# DI-004: Esquema GraphQL
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Schema Completo
|
||||
|
||||
```graphql
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# TYPES
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
type Student {
|
||||
id: Int!
|
||||
name: String!
|
||||
email: String!
|
||||
totalCredits: Int!
|
||||
enrollments: [Enrollment!]!
|
||||
}
|
||||
|
||||
type Subject {
|
||||
id: Int!
|
||||
name: String!
|
||||
credits: Int!
|
||||
professor: Professor!
|
||||
enrolledStudents: [Student!]!
|
||||
}
|
||||
|
||||
type Professor {
|
||||
id: Int!
|
||||
name: String!
|
||||
subjects: [Subject!]!
|
||||
}
|
||||
|
||||
type Enrollment {
|
||||
id: Int!
|
||||
student: Student!
|
||||
subject: Subject!
|
||||
enrolledAt: DateTime!
|
||||
}
|
||||
|
||||
type AvailableSubject {
|
||||
subject: Subject!
|
||||
isAvailable: Boolean!
|
||||
unavailableReason: String
|
||||
}
|
||||
|
||||
type Classmate {
|
||||
subjectName: String!
|
||||
students: [String!]!
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# QUERIES
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
type Query {
|
||||
# Estudiantes
|
||||
students: [Student!]!
|
||||
student(id: Int!): Student
|
||||
|
||||
# Materias
|
||||
subjects: [Subject!]!
|
||||
subject(id: Int!): Subject
|
||||
availableSubjects(studentId: Int!): [AvailableSubject!]!
|
||||
|
||||
# Profesores
|
||||
professors: [Professor!]!
|
||||
professor(id: Int!): Professor
|
||||
|
||||
# Compañeros de clase
|
||||
classmates(studentId: Int!): [Classmate!]!
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# MUTATIONS
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
type Mutation {
|
||||
# Estudiantes
|
||||
createStudent(input: CreateStudentInput!): StudentPayload!
|
||||
updateStudent(id: Int!, input: UpdateStudentInput!): StudentPayload!
|
||||
deleteStudent(id: Int!): DeletePayload!
|
||||
|
||||
# Inscripciones
|
||||
enrollStudent(input: EnrollInput!): EnrollmentPayload!
|
||||
unenrollStudent(enrollmentId: Int!): DeletePayload!
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# INPUTS
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
input CreateStudentInput {
|
||||
name: String!
|
||||
email: String!
|
||||
}
|
||||
|
||||
input UpdateStudentInput {
|
||||
name: String
|
||||
email: String
|
||||
}
|
||||
|
||||
input EnrollInput {
|
||||
studentId: Int!
|
||||
subjectId: Int!
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# PAYLOADS (Union para errores)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
type StudentPayload {
|
||||
student: Student
|
||||
errors: [String!]
|
||||
}
|
||||
|
||||
type EnrollmentPayload {
|
||||
enrollment: Enrollment
|
||||
errors: [String!]
|
||||
}
|
||||
|
||||
type DeletePayload {
|
||||
success: Boolean!
|
||||
errors: [String!]
|
||||
}
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# SCALARS
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
scalar DateTime
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Ejemplos de Queries
|
||||
|
||||
### Listar estudiantes con inscripciones
|
||||
```graphql
|
||||
query GetStudents {
|
||||
students {
|
||||
id
|
||||
name
|
||||
email
|
||||
totalCredits
|
||||
enrollments {
|
||||
subject { name professor { name } }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Materias disponibles para inscripción
|
||||
```graphql
|
||||
query GetAvailableSubjects($studentId: Int!) {
|
||||
availableSubjects(studentId: $studentId) {
|
||||
subject { id name credits professor { name } }
|
||||
isAvailable
|
||||
unavailableReason
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Compañeros de clase
|
||||
```graphql
|
||||
query GetClassmates($studentId: Int!) {
|
||||
classmates(studentId: $studentId) {
|
||||
subjectName
|
||||
students
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Ejemplos de Mutations
|
||||
|
||||
### Crear estudiante
|
||||
```graphql
|
||||
mutation CreateStudent {
|
||||
createStudent(input: { name: "Juan Pérez", email: "juan@email.com" }) {
|
||||
student { id name email }
|
||||
errors
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Inscribir en materia
|
||||
```graphql
|
||||
mutation Enroll {
|
||||
enrollStudent(input: { studentId: 1, subjectId: 3 }) {
|
||||
enrollment {
|
||||
id
|
||||
subject { name }
|
||||
enrolledAt
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. DataLoaders (Evitar N+1)
|
||||
|
||||
| DataLoader | Carga Batch |
|
||||
|------------|-------------|
|
||||
| `StudentByIdDataLoader` | Estudiantes por IDs |
|
||||
| `SubjectByIdDataLoader` | Materias por IDs |
|
||||
| `ProfessorByIdDataLoader` | Profesores por IDs |
|
||||
| `EnrollmentsByStudentDataLoader` | Inscripciones por estudiante |
|
||||
| `SubjectsByProfessorDataLoader` | Materias por profesor |
|
||||
|
||||
---
|
||||
|
||||
## 5. Errores de Negocio
|
||||
|
||||
| Código | Mensaje | Contexto |
|
||||
|--------|---------|----------|
|
||||
| `MAX_ENROLLMENTS` | "Máximo 3 materias permitidas" | enrollStudent |
|
||||
| `SAME_PROFESSOR` | "Ya tienes materia con este profesor" | enrollStudent |
|
||||
| `DUPLICATE_EMAIL` | "Email ya registrado" | createStudent |
|
||||
| `NOT_FOUND` | "Estudiante no encontrado" | updateStudent |
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
# DI-007: Contratos y DTOs
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. DTOs Backend (C#)
|
||||
|
||||
### Request DTOs (Inputs)
|
||||
|
||||
```csharp
|
||||
// Students
|
||||
public record CreateStudentInput(string Name, string Email);
|
||||
public record UpdateStudentInput(string? Name, string? Email);
|
||||
|
||||
// Enrollments
|
||||
public record EnrollInput(int StudentId, int SubjectId);
|
||||
```
|
||||
|
||||
### Response DTOs
|
||||
|
||||
```csharp
|
||||
// Student
|
||||
public record StudentDto(
|
||||
int Id,
|
||||
string Name,
|
||||
string Email,
|
||||
int TotalCredits,
|
||||
IEnumerable<EnrollmentDto> Enrollments);
|
||||
|
||||
// Subject
|
||||
public record SubjectDto(
|
||||
int Id,
|
||||
string Name,
|
||||
int Credits,
|
||||
ProfessorDto Professor);
|
||||
|
||||
// Professor
|
||||
public record ProfessorDto(
|
||||
int Id,
|
||||
string Name);
|
||||
|
||||
// Enrollment
|
||||
public record EnrollmentDto(
|
||||
int Id,
|
||||
SubjectDto Subject,
|
||||
DateTime EnrolledAt);
|
||||
|
||||
// Available Subject (con disponibilidad)
|
||||
public record AvailableSubjectDto(
|
||||
SubjectDto Subject,
|
||||
bool IsAvailable,
|
||||
string? UnavailableReason);
|
||||
|
||||
// Classmates
|
||||
public record ClassmateDto(
|
||||
string SubjectName,
|
||||
IEnumerable<string> StudentNames);
|
||||
```
|
||||
|
||||
### Payloads (Respuestas con errores)
|
||||
|
||||
```csharp
|
||||
public record StudentPayload(StudentDto? Student, IEnumerable<string>? Errors);
|
||||
public record EnrollmentPayload(EnrollmentDto? Enrollment, IEnumerable<string>? Errors);
|
||||
public record DeletePayload(bool Success, IEnumerable<string>? Errors);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. DTOs Frontend (TypeScript)
|
||||
|
||||
### Tipos Generados (graphql-codegen)
|
||||
|
||||
```typescript
|
||||
// Student
|
||||
export interface Student {
|
||||
id: number;
|
||||
name: string;
|
||||
email: string;
|
||||
totalCredits: number;
|
||||
enrollments: Enrollment[];
|
||||
}
|
||||
|
||||
// Subject
|
||||
export interface Subject {
|
||||
id: number;
|
||||
name: string;
|
||||
credits: number;
|
||||
professor: Professor;
|
||||
}
|
||||
|
||||
// Professor
|
||||
export interface Professor {
|
||||
id: number;
|
||||
name: string;
|
||||
subjects?: Subject[];
|
||||
}
|
||||
|
||||
// Enrollment
|
||||
export interface Enrollment {
|
||||
id: number;
|
||||
subject: Subject;
|
||||
enrolledAt: string;
|
||||
}
|
||||
|
||||
// Available Subject
|
||||
export interface AvailableSubject {
|
||||
subject: Subject;
|
||||
isAvailable: boolean;
|
||||
unavailableReason?: string;
|
||||
}
|
||||
|
||||
// Classmate
|
||||
export interface Classmate {
|
||||
subjectName: string;
|
||||
students: string[];
|
||||
}
|
||||
```
|
||||
|
||||
### Inputs
|
||||
|
||||
```typescript
|
||||
export interface CreateStudentInput {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export interface UpdateStudentInput {
|
||||
name?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
export interface EnrollInput {
|
||||
studentId: number;
|
||||
subjectId: number;
|
||||
}
|
||||
```
|
||||
|
||||
### Payloads
|
||||
|
||||
```typescript
|
||||
export interface StudentPayload {
|
||||
student?: Student;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface EnrollmentPayload {
|
||||
enrollment?: Enrollment;
|
||||
errors?: string[];
|
||||
}
|
||||
|
||||
export interface DeletePayload {
|
||||
success: boolean;
|
||||
errors?: string[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Mapeos (Mapster)
|
||||
|
||||
```csharp
|
||||
public class MappingConfig : IRegister
|
||||
{
|
||||
public void Register(TypeAdapterConfig config)
|
||||
{
|
||||
config.NewConfig<Student, StudentDto>()
|
||||
.Map(dest => dest.TotalCredits, src => src.Enrollments.Count * 3);
|
||||
|
||||
config.NewConfig<Subject, SubjectDto>();
|
||||
config.NewConfig<Professor, ProfessorDto>();
|
||||
config.NewConfig<Enrollment, EnrollmentDto>();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Validadores (FluentValidation)
|
||||
|
||||
```csharp
|
||||
public class CreateStudentInputValidator : AbstractValidator<CreateStudentInput>
|
||||
{
|
||||
public CreateStudentInputValidator()
|
||||
{
|
||||
RuleFor(x => x.Name)
|
||||
.NotEmpty().WithMessage("Nombre requerido")
|
||||
.MaximumLength(100).WithMessage("Máximo 100 caracteres");
|
||||
|
||||
RuleFor(x => x.Email)
|
||||
.NotEmpty().WithMessage("Email requerido")
|
||||
.EmailAddress().WithMessage("Formato de email inválido")
|
||||
.MaximumLength(255).WithMessage("Máximo 255 caracteres");
|
||||
}
|
||||
}
|
||||
|
||||
public class EnrollInputValidator : AbstractValidator<EnrollInput>
|
||||
{
|
||||
public EnrollInputValidator()
|
||||
{
|
||||
RuleFor(x => x.StudentId)
|
||||
.GreaterThan(0).WithMessage("StudentId inválido");
|
||||
|
||||
RuleFor(x => x.SubjectId)
|
||||
.GreaterThan(0).WithMessage("SubjectId inválido");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Contratos de Repositorio (Ports)
|
||||
|
||||
```csharp
|
||||
public interface IStudentRepository
|
||||
{
|
||||
Task<Student?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
Task<Student?> GetByIdWithEnrollmentsAsync(int id, CancellationToken ct = default);
|
||||
Task<IEnumerable<Student>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<bool> EmailExistsAsync(string email, int? excludeId = null, CancellationToken ct = default);
|
||||
Task AddAsync(Student student, CancellationToken ct = default);
|
||||
void Update(Student student);
|
||||
void Delete(Student student);
|
||||
}
|
||||
|
||||
public interface ISubjectRepository
|
||||
{
|
||||
Task<Subject?> GetByIdAsync(int id, CancellationToken ct = default);
|
||||
Task<IEnumerable<Subject>> GetAllAsync(CancellationToken ct = default);
|
||||
Task<IEnumerable<Subject>> GetByProfessorIdAsync(int professorId, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public interface IEnrollmentRepository
|
||||
{
|
||||
Task<IEnumerable<Enrollment>> GetByStudentIdAsync(int studentId, CancellationToken ct = default);
|
||||
Task<IEnumerable<Student>> GetClassmatesAsync(int studentId, int subjectId, CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
Task<int> SaveChangesAsync(CancellationToken ct = default);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Resumen de Contratos por Operación
|
||||
|
||||
| Operación | Input | Output | Errores Posibles |
|
||||
|-----------|-------|--------|------------------|
|
||||
| createStudent | CreateStudentInput | StudentPayload | DUPLICATE_EMAIL, VALIDATION |
|
||||
| updateStudent | UpdateStudentInput | StudentPayload | NOT_FOUND, DUPLICATE_EMAIL |
|
||||
| deleteStudent | id: Int | DeletePayload | NOT_FOUND |
|
||||
| enrollStudent | EnrollInput | EnrollmentPayload | MAX_ENROLLMENTS, SAME_PROFESSOR |
|
||||
| unenrollStudent | enrollmentId: Int | DeletePayload | NOT_FOUND |
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
# 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 │
|
||||
│ │ RowVersion │
|
||||
│ 1:2 └────────┬────────┘
|
||||
▼ │
|
||||
┌─────────────────┐ │ 0..3
|
||||
│ SUBJECT │ ▼
|
||||
├─────────────────┤ ┌─────────────────┐
|
||||
│ Id: int (PK) │◄──────│ ENROLLMENT │
|
||||
│ Name: string │ 1:N ├─────────────────┤
|
||||
│ Credits: 3 │ │ Id: int (PK) │
|
||||
│ ProfessorId: FK │ │ StudentId: FK │
|
||||
└─────────────────┘ │ SubjectId: FK │
|
||||
│ EnrolledAt │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Entidades
|
||||
|
||||
### Student (Aggregate Root)
|
||||
|
||||
```csharp
|
||||
public class Student
|
||||
{
|
||||
public int Id { get; private set; }
|
||||
public string Name { get; private set; }
|
||||
public Email Email { get; private set; }
|
||||
public byte[] RowVersion { get; private set; }
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 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 |
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
# DV-001: Configuración del Repositorio
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Estructura de Carpetas
|
||||
|
||||
```
|
||||
/
|
||||
├── src/
|
||||
│ ├── backend/
|
||||
│ │ ├── Domain/
|
||||
│ │ ├── Application/
|
||||
│ │ ├── Adapters/
|
||||
│ │ │ ├── Driving/Api/
|
||||
│ │ │ └── Driven/Persistence/
|
||||
│ │ └── Host/
|
||||
│ └── frontend/
|
||||
│ └── src/app/
|
||||
│ ├── core/
|
||||
│ ├── shared/
|
||||
│ └── features/
|
||||
├── tests/
|
||||
│ ├── Domain.Tests/
|
||||
│ ├── Application.Tests/
|
||||
│ ├── Adapters.Tests/
|
||||
│ └── e2e/
|
||||
├── docs/
|
||||
│ └── entregables/
|
||||
├── database/
|
||||
│ ├── scripts/
|
||||
│ └── migrations/
|
||||
├── deploy/
|
||||
│ └── docker/
|
||||
├── CLAUDE.md
|
||||
├── README.md
|
||||
└── .gitignore
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. .gitignore
|
||||
|
||||
```gitignore
|
||||
# .NET
|
||||
bin/
|
||||
obj/
|
||||
*.user
|
||||
*.suo
|
||||
.vs/
|
||||
*.csproj.user
|
||||
|
||||
# Angular
|
||||
node_modules/
|
||||
dist/
|
||||
.angular/
|
||||
.nx/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
appsettings.*.json
|
||||
!appsettings.json
|
||||
!appsettings.Development.json.example
|
||||
|
||||
# Database
|
||||
*.mdf
|
||||
*.ldf
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Test
|
||||
coverage/
|
||||
TestResults/
|
||||
|
||||
# Build
|
||||
publish/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Comandos de Inicialización
|
||||
|
||||
```bash
|
||||
# Crear repositorio
|
||||
git init
|
||||
git add .
|
||||
git commit -m "chore: initial project structure"
|
||||
|
||||
# Crear rama de desarrollo
|
||||
git checkout -b develop
|
||||
|
||||
# Estructura de ramas
|
||||
# main → producción
|
||||
# develop → integración
|
||||
# feature/ → nuevas funcionalidades
|
||||
# fix/ → correcciones
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Convenciones de Commits
|
||||
|
||||
```
|
||||
<tipo>(<alcance>): <descripción>
|
||||
|
||||
Tipos:
|
||||
- feat: Nueva funcionalidad
|
||||
- fix: Corrección de bug
|
||||
- refactor: Refactorización
|
||||
- test: Tests
|
||||
- docs: Documentación
|
||||
- chore: Tareas de mantenimiento
|
||||
|
||||
Ejemplos:
|
||||
feat(students): add create student mutation
|
||||
fix(enrollment): validate professor constraint
|
||||
test(domain): add enrollment policy tests
|
||||
```
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
# DV-002: Configuración Solución .NET 10
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Crear Solución y Proyectos
|
||||
|
||||
```bash
|
||||
cd src/backend
|
||||
|
||||
# Crear solución
|
||||
dotnet new sln -n StudentEnrollment
|
||||
|
||||
# Crear proyectos
|
||||
dotnet new classlib -n Domain -f net10.0
|
||||
dotnet new classlib -n Application -f net10.0
|
||||
dotnet new classlib -n Adapters.Driving.Api -f net10.0
|
||||
dotnet new classlib -n Adapters.Driven.Persistence -f net10.0
|
||||
dotnet new web -n Host -f net10.0
|
||||
|
||||
# Agregar a solución
|
||||
dotnet sln add Domain/Domain.csproj
|
||||
dotnet sln add Application/Application.csproj
|
||||
dotnet sln add Adapters.Driving.Api/Adapters.Driving.Api.csproj
|
||||
dotnet sln add Adapters.Driven.Persistence/Adapters.Driven.Persistence.csproj
|
||||
dotnet sln add Host/Host.csproj
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Referencias entre Proyectos
|
||||
|
||||
```bash
|
||||
# Application → Domain
|
||||
dotnet add Application reference Domain
|
||||
|
||||
# Adapters.Driving.Api → Application
|
||||
dotnet add Adapters.Driving.Api reference Application
|
||||
|
||||
# Adapters.Driven.Persistence → Domain
|
||||
dotnet add Adapters.Driven.Persistence reference Domain
|
||||
|
||||
# Host → Todos
|
||||
dotnet add Host reference Application
|
||||
dotnet add Host reference Adapters.Driving.Api
|
||||
dotnet add Host reference Adapters.Driven.Persistence
|
||||
```
|
||||
|
||||
```
|
||||
Host
|
||||
├── Adapters.Driving.Api → Application → Domain
|
||||
└── Adapters.Driven.Persistence ────────→ Domain
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Paquetes NuGet por Proyecto
|
||||
|
||||
### Domain (sin dependencias externas)
|
||||
```xml
|
||||
<!-- Solo C# puro -->
|
||||
```
|
||||
|
||||
### Application
|
||||
```xml
|
||||
<PackageReference Include="FluentValidation" />
|
||||
<PackageReference Include="Mapster" />
|
||||
<PackageReference Include="MediatR" />
|
||||
```
|
||||
|
||||
### Adapters.Driven.Persistence
|
||||
```xml
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" />
|
||||
```
|
||||
|
||||
### Adapters.Driving.Api
|
||||
```xml
|
||||
<PackageReference Include="HotChocolate.AspNetCore" />
|
||||
<PackageReference Include="HotChocolate.Data" />
|
||||
<PackageReference Include="HotChocolate.Data.EntityFramework" />
|
||||
```
|
||||
|
||||
### Host
|
||||
```xml
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Directory.Build.props (Raíz backend)
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Program.cs (Host)
|
||||
|
||||
```csharp
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Services
|
||||
builder.Services.AddApplication();
|
||||
builder.Services.AddPersistence(builder.Configuration);
|
||||
builder.Services.AddGraphQLApi();
|
||||
|
||||
// CORS
|
||||
builder.Services.AddCors(options =>
|
||||
{
|
||||
options.AddDefaultPolicy(policy =>
|
||||
policy.WithOrigins("http://localhost:4200")
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod());
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.UseCors();
|
||||
app.MapGraphQL();
|
||||
|
||||
app.Run();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Comandos de Desarrollo
|
||||
|
||||
```bash
|
||||
# Restaurar dependencias
|
||||
dotnet restore
|
||||
|
||||
# Build
|
||||
dotnet build
|
||||
|
||||
# Ejecutar
|
||||
dotnet run --project Host
|
||||
|
||||
# Watch mode
|
||||
dotnet watch run --project Host
|
||||
|
||||
# Tests
|
||||
dotnet test
|
||||
|
||||
# Migraciones EF
|
||||
dotnet ef migrations add Initial -p Adapters.Driven.Persistence -s Host
|
||||
dotnet ef database update -p Adapters.Driven.Persistence -s Host
|
||||
```
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
# DV-003: Configuración Proyecto Angular 21
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Crear Proyecto
|
||||
|
||||
```bash
|
||||
cd src/frontend
|
||||
|
||||
# Crear proyecto Angular 21
|
||||
ng new student-enrollment \
|
||||
--standalone \
|
||||
--style=scss \
|
||||
--routing \
|
||||
--ssr=false
|
||||
|
||||
cd student-enrollment
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Instalar Dependencias
|
||||
|
||||
```bash
|
||||
# Angular Material
|
||||
ng add @angular/material
|
||||
|
||||
# Apollo GraphQL
|
||||
npm install apollo-angular @apollo/client graphql
|
||||
|
||||
# GraphQL Code Generator
|
||||
npm install -D @graphql-codegen/cli \
|
||||
@graphql-codegen/typescript \
|
||||
@graphql-codegen/typescript-operations \
|
||||
@graphql-codegen/typescript-apollo-angular
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Estructura de Carpetas
|
||||
|
||||
```bash
|
||||
# Crear estructura
|
||||
mkdir -p src/app/core/{services,graphql/{queries,mutations},interceptors}
|
||||
mkdir -p src/app/shared/{components,pipes,directives}
|
||||
mkdir -p src/app/features/{students,enrollment,classmates}/{pages,components}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Configuración Apollo (app.config.ts)
|
||||
|
||||
```typescript
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideApollo } from 'apollo-angular';
|
||||
import { HttpLink } from 'apollo-angular/http';
|
||||
import { InMemoryCache } from '@apollo/client/core';
|
||||
import { inject } from '@angular/core';
|
||||
import { routes } from './app.routes';
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideRouter(routes),
|
||||
provideHttpClient(),
|
||||
provideApollo(() => {
|
||||
const httpLink = inject(HttpLink);
|
||||
return {
|
||||
link: httpLink.create({ uri: environment.graphqlUrl }),
|
||||
cache: new InMemoryCache(),
|
||||
};
|
||||
}),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Environment Files
|
||||
|
||||
```typescript
|
||||
// environments/environment.ts
|
||||
export const environment = {
|
||||
production: false,
|
||||
graphqlUrl: 'https://localhost:5001/graphql',
|
||||
};
|
||||
|
||||
// environments/environment.prod.ts
|
||||
export const environment = {
|
||||
production: true,
|
||||
graphqlUrl: '/graphql',
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. GraphQL Codegen (codegen.ts)
|
||||
|
||||
```typescript
|
||||
import type { CodegenConfig } from '@graphql-codegen/cli';
|
||||
|
||||
const config: CodegenConfig = {
|
||||
schema: 'https://localhost:5001/graphql',
|
||||
documents: 'src/app/core/graphql/**/*.graphql',
|
||||
generates: {
|
||||
'src/app/core/graphql/generated/types.ts': {
|
||||
plugins: [
|
||||
'typescript',
|
||||
'typescript-operations',
|
||||
'typescript-apollo-angular',
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
```json
|
||||
// package.json scripts
|
||||
{
|
||||
"scripts": {
|
||||
"codegen": "graphql-codegen --config codegen.ts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Path Aliases (tsconfig.json)
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@core/*": ["src/app/core/*"],
|
||||
"@shared/*": ["src/app/shared/*"],
|
||||
"@features/*": ["src/app/features/*"],
|
||||
"@env/*": ["src/environments/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Comandos de Desarrollo
|
||||
|
||||
```bash
|
||||
# Desarrollo
|
||||
ng serve
|
||||
|
||||
# Build producción
|
||||
ng build --configuration production
|
||||
|
||||
# Generar tipos GraphQL
|
||||
npm run codegen
|
||||
|
||||
# Lint
|
||||
ng lint
|
||||
|
||||
# Tests
|
||||
ng test
|
||||
ng test --watch=false --code-coverage
|
||||
|
||||
# Generar componente standalone
|
||||
ng g c features/students/pages/student-list --standalone
|
||||
```
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
# DV-004: Configuración Base de Datos
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Docker Compose (SQL Server)
|
||||
|
||||
```yaml
|
||||
# deploy/docker/docker-compose.yml
|
||||
services:
|
||||
sqlserver:
|
||||
image: mcr.microsoft.com/mssql/server
|
||||
container_name: sqlserver-students
|
||||
environment:
|
||||
- ACCEPT_EULA=Y
|
||||
- SA_PASSWORD=${DB_PASSWORD:-YourStrong@Passw0rd}
|
||||
- MSSQL_PID=Developer
|
||||
ports:
|
||||
- "1433:1433"
|
||||
volumes:
|
||||
- sqlserver-data:/var/opt/mssql
|
||||
healthcheck:
|
||||
test: /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$$SA_PASSWORD" -C -Q "SELECT 1"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
sqlserver-data:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Comandos Docker
|
||||
|
||||
```bash
|
||||
# Iniciar SQL Server
|
||||
docker-compose -f deploy/docker/docker-compose.yml up -d sqlserver
|
||||
|
||||
# Ver logs
|
||||
docker logs sqlserver-students
|
||||
|
||||
# Conectar a SQL Server
|
||||
docker exec -it sqlserver-students /opt/mssql-tools18/bin/sqlcmd \
|
||||
-S localhost -U sa -P 'YourStrong@Passw0rd' -C
|
||||
|
||||
# Detener
|
||||
docker-compose -f deploy/docker/docker-compose.yml down
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Connection String
|
||||
|
||||
```json
|
||||
// appsettings.Development.json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost;Database=StudentEnrollment;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. DbContext Configuration
|
||||
|
||||
```csharp
|
||||
// Adapters/Driven/Persistence/Context/AppDbContext.cs
|
||||
public class AppDbContext : DbContext
|
||||
{
|
||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
|
||||
|
||||
public DbSet<Student> Students => Set<Student>();
|
||||
public DbSet<Subject> Subjects => Set<Subject>();
|
||||
public DbSet<Professor> Professors => Set<Professor>();
|
||||
public DbSet<Enrollment> Enrollments => Set<Enrollment>();
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Registrar DbContext
|
||||
|
||||
```csharp
|
||||
// Adapters/Driven/Persistence/DependencyInjection.cs
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddPersistence(
|
||||
this IServiceCollection services,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
services.AddDbContext<AppDbContext>(options =>
|
||||
options.UseSqlServer(
|
||||
configuration.GetConnectionString("DefaultConnection")));
|
||||
|
||||
services.AddScoped<IStudentRepository, StudentRepository>();
|
||||
services.AddScoped<ISubjectRepository, SubjectRepository>();
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Migraciones
|
||||
|
||||
```bash
|
||||
# Crear migración inicial
|
||||
dotnet ef migrations add InitialCreate \
|
||||
-p src/backend/Adapters.Driven.Persistence \
|
||||
-s src/backend/Host
|
||||
|
||||
# Aplicar migración
|
||||
dotnet ef database update \
|
||||
-p src/backend/Adapters.Driven.Persistence \
|
||||
-s src/backend/Host
|
||||
|
||||
# Generar script SQL
|
||||
dotnet ef migrations script \
|
||||
-p src/backend/Adapters.Driven.Persistence \
|
||||
-s src/backend/Host \
|
||||
-o database/scripts/create.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Seed Data
|
||||
|
||||
```csharp
|
||||
// Adapters/Driven/Persistence/Seeding/DataSeeder.cs
|
||||
public static class DataSeeder
|
||||
{
|
||||
public static void Seed(ModelBuilder modelBuilder)
|
||||
{
|
||||
// Profesores
|
||||
modelBuilder.Entity<Professor>().HasData(
|
||||
new { Id = 1, Name = "Dr. García" },
|
||||
new { Id = 2, Name = "Dra. Martínez" },
|
||||
new { Id = 3, Name = "Dr. López" },
|
||||
new { Id = 4, Name = "Dra. Rodríguez" },
|
||||
new { Id = 5, Name = "Dr. Hernández" }
|
||||
);
|
||||
|
||||
// Materias (2 por profesor)
|
||||
modelBuilder.Entity<Subject>().HasData(
|
||||
new { Id = 1, Name = "Matemáticas I", Credits = 3, ProfessorId = 1 },
|
||||
new { Id = 2, Name = "Matemáticas II", Credits = 3, ProfessorId = 1 },
|
||||
new { Id = 3, Name = "Física I", Credits = 3, ProfessorId = 2 },
|
||||
new { Id = 4, Name = "Física II", Credits = 3, ProfessorId = 2 },
|
||||
new { Id = 5, Name = "Programación I", Credits = 3, ProfessorId = 3 },
|
||||
new { Id = 6, Name = "Programación II", Credits = 3, ProfessorId = 3 },
|
||||
new { Id = 7, Name = "Base de Datos I", Credits = 3, ProfessorId = 4 },
|
||||
new { Id = 8, Name = "Base de Datos II", Credits = 3, ProfessorId = 4 },
|
||||
new { Id = 9, Name = "Redes I", Credits = 3, ProfessorId = 5 },
|
||||
new { Id = 10, Name = "Redes II", Credits = 3, ProfessorId = 5 }
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
# DV-005: Variables de Entorno
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Backend (.NET 10)
|
||||
|
||||
### appsettings.json (Base)
|
||||
|
||||
```json
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning",
|
||||
"HotChocolate": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"GraphQL": {
|
||||
"MaxExecutionDepth": 5,
|
||||
"MaxComplexity": 100
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### appsettings.Development.json
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=localhost;Database=StudentEnrollment;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True"
|
||||
},
|
||||
"GraphQL": {
|
||||
"EnableIntrospection": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### appsettings.Production.json.example
|
||||
|
||||
```json
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Server=PROD_SERVER;Database=StudentEnrollment;User Id=app_user;Password=CHANGE_ME;Encrypt=True"
|
||||
},
|
||||
"GraphQL": {
|
||||
"EnableIntrospection": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. User Secrets (Desarrollo Local)
|
||||
|
||||
```bash
|
||||
# Inicializar secrets
|
||||
cd src/backend/Host
|
||||
dotnet user-secrets init
|
||||
|
||||
# Guardar connection string
|
||||
dotnet user-secrets set "ConnectionStrings:DefaultConnection" \
|
||||
"Server=localhost;Database=StudentEnrollment;User Id=sa;Password=YourStrong@Passw0rd;TrustServerCertificate=True"
|
||||
|
||||
# Listar secrets
|
||||
dotnet user-secrets list
|
||||
|
||||
# Remover
|
||||
dotnet user-secrets remove "ConnectionStrings:DefaultConnection"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Frontend (Angular 21)
|
||||
|
||||
### environment.ts (Desarrollo)
|
||||
|
||||
```typescript
|
||||
export const environment = {
|
||||
production: false,
|
||||
graphqlUrl: 'https://localhost:5001/graphql',
|
||||
apiTimeout: 30000,
|
||||
};
|
||||
```
|
||||
|
||||
### environment.prod.ts (Producción)
|
||||
|
||||
```typescript
|
||||
export const environment = {
|
||||
production: true,
|
||||
graphqlUrl: '/graphql',
|
||||
apiTimeout: 15000,
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Docker (.env)
|
||||
|
||||
### .env.example
|
||||
|
||||
```env
|
||||
# Database
|
||||
DB_PASSWORD=YourStrong@Passw0rd
|
||||
DB_NAME=StudentEnrollment
|
||||
|
||||
# API
|
||||
ASPNETCORE_ENVIRONMENT=Development
|
||||
ASPNETCORE_URLS=http://+:5000
|
||||
|
||||
# Frontend
|
||||
FRONTEND_URL=http://localhost:4200
|
||||
```
|
||||
|
||||
### docker-compose.yml (usando .env)
|
||||
|
||||
```yaml
|
||||
services:
|
||||
sqlserver:
|
||||
environment:
|
||||
- SA_PASSWORD=${DB_PASSWORD}
|
||||
|
||||
api:
|
||||
environment:
|
||||
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT}
|
||||
- ConnectionStrings__DefaultConnection=Server=sqlserver;Database=${DB_NAME};User Id=sa;Password=${DB_PASSWORD};TrustServerCertificate=True
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Variables por Ambiente
|
||||
|
||||
| Variable | Desarrollo | Producción |
|
||||
|----------|------------|------------|
|
||||
| ConnectionString | localhost | Servidor prod |
|
||||
| EnableIntrospection | true | false |
|
||||
| LogLevel | Debug | Warning |
|
||||
| CORS Origins | localhost:4200 | dominio.com |
|
||||
| MaxExecutionDepth | 10 | 5 |
|
||||
|
||||
---
|
||||
|
||||
## 6. Cargar Configuración
|
||||
|
||||
```csharp
|
||||
// Program.cs
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Orden de carga (último gana):
|
||||
// 1. appsettings.json
|
||||
// 2. appsettings.{Environment}.json
|
||||
// 3. User Secrets (solo Development)
|
||||
// 4. Variables de entorno
|
||||
// 5. Command line args
|
||||
|
||||
var connectionString = builder.Configuration
|
||||
.GetConnectionString("DefaultConnection");
|
||||
```
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
# DV-006: Herramientas de Calidad
|
||||
|
||||
**Proyecto:** Sistema de Registro de Estudiantes
|
||||
**Fecha:** 2026-01-07
|
||||
|
||||
---
|
||||
|
||||
## 1. Backend (.NET 10)
|
||||
|
||||
### .editorconfig
|
||||
|
||||
```ini
|
||||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{cs,csx}]
|
||||
csharp_style_var_when_type_is_apparent = true:suggestion
|
||||
csharp_prefer_braces = true:warning
|
||||
dotnet_sort_system_directives_first = true
|
||||
```
|
||||
|
||||
### Directory.Build.props (Analyzers)
|
||||
|
||||
```xml
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
|
||||
<AnalysisLevel>latest</AnalysisLevel>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
### Comandos
|
||||
|
||||
```bash
|
||||
# Formatear código
|
||||
dotnet format
|
||||
|
||||
# Verificar sin cambiar
|
||||
dotnet format --verify-no-changes
|
||||
|
||||
# Build con warnings
|
||||
dotnet build -warnaserror
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Frontend (Angular 21)
|
||||
|
||||
### ESLint
|
||||
|
||||
```bash
|
||||
# Instalar
|
||||
ng add @angular-eslint/schematics
|
||||
```
|
||||
|
||||
```json
|
||||
// .eslintrc.json
|
||||
{
|
||||
"root": true,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@angular-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/component-selector": ["error", {
|
||||
"prefix": "app",
|
||||
"style": "kebab-case",
|
||||
"type": "element"
|
||||
}],
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"no-console": "warn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Prettier
|
||||
|
||||
```json
|
||||
// .prettierrc
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"printWidth": 100
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// .prettierignore
|
||||
node_modules
|
||||
dist
|
||||
coverage
|
||||
.angular
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Pre-commit Hooks (Husky)
|
||||
|
||||
```bash
|
||||
# Instalar
|
||||
npm install -D husky lint-staged
|
||||
npx husky init
|
||||
```
|
||||
|
||||
```json
|
||||
// package.json
|
||||
{
|
||||
"lint-staged": {
|
||||
"*.ts": ["eslint --fix", "prettier --write"],
|
||||
"*.html": ["prettier --write"],
|
||||
"*.scss": ["prettier --write"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# .husky/pre-commit
|
||||
npm run lint-staged
|
||||
cd ../backend && dotnet format --verify-no-changes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Scripts package.json
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint",
|
||||
"format": "prettier --write \"src/**/*.{ts,html,scss}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{ts,html,scss}\"",
|
||||
"codegen": "graphql-codegen",
|
||||
"prepare": "husky"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. CI Checks
|
||||
|
||||
```yaml
|
||||
# .github/workflows/ci.yml (ejemplo)
|
||||
jobs:
|
||||
backend:
|
||||
steps:
|
||||
- run: dotnet format --verify-no-changes
|
||||
- run: dotnet build -warnaserror
|
||||
- run: dotnet test
|
||||
|
||||
frontend:
|
||||
steps:
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run format:check
|
||||
- run: npm run build
|
||||
- run: npm test -- --watch=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Checklist de Calidad
|
||||
|
||||
| Verificación | Backend | Frontend |
|
||||
|--------------|---------|----------|
|
||||
| Formato código | `dotnet format` | `prettier` |
|
||||
| Linting | Roslyn Analyzers | ESLint |
|
||||
| Tipos | Nullable enabled | TypeScript strict |
|
||||
| Tests | xUnit | Jasmine/Jest |
|
||||
| Pre-commit | dotnet format | lint-staged |
|
||||