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 |
|
||||||