- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
510 lines
16 KiB
Markdown
510 lines
16 KiB
Markdown
# BE-P2-008: Notificaciones Push/Email para Docentes - Reporte de Implementación
|
|
|
|
**Sprint:** P2-B
|
|
**Story Points:** 3 SP
|
|
**Fecha:** 2025-12-05
|
|
**Estado:** ✅ Completado
|
|
|
|
---
|
|
|
|
## Resumen Ejecutivo
|
|
|
|
Se implementó exitosamente el sistema de notificaciones push/email para docentes cuando estudiantes envían ejercicios M4-M5 que requieren revisión manual.
|
|
|
|
### Características Implementadas
|
|
|
|
1. ✅ **Detección automática** de ejercicios que requieren revisión manual (`requires_manual_grading = true`)
|
|
2. ✅ **Notificación in-app** (siempre enviada)
|
|
3. ✅ **Notificación por email** (respeta preferencias del docente)
|
|
4. ✅ **Información completa** en la notificación:
|
|
- Nombre del estudiante
|
|
- Título y tipo de ejercicio
|
|
- Enlace directo a la página de revisión
|
|
- Nombre del aula (classroom)
|
|
5. ✅ **Manejo robusto de errores** (no bloquea el envío de ejercicios)
|
|
6. ✅ **Respeto a preferencias de usuario** para envío de emails
|
|
|
|
---
|
|
|
|
## Arquitectura de la Solución
|
|
|
|
### Flujo de Notificación
|
|
|
|
```
|
|
Estudiante envía ejercicio M4-M5
|
|
↓
|
|
ExerciseSubmissionService.submitExercise()
|
|
↓
|
|
Verificar: exercise.requires_manual_grading = true
|
|
↓
|
|
notifyTeacherOfSubmission()
|
|
↓
|
|
┌───────────────────────┐
|
|
│ 1. Obtener estudiante │
|
|
│ (nombre, email) │
|
|
└───────┬───────────────┘
|
|
↓
|
|
┌───────────────────────────────┐
|
|
│ 2. Query: Obtener docente │
|
|
│ asignado desde classroom │
|
|
│ (classroom_members + │
|
|
│ teacher_classrooms) │
|
|
└───────┬───────────────────────┘
|
|
↓
|
|
┌───────────────────────────────┐
|
|
│ 3. Enviar notificación in-app │
|
|
│ (NotificationsService) │
|
|
└───────┬───────────────────────┘
|
|
↓
|
|
┌───────────────────────────────┐
|
|
│ 4. Verificar preferencias │
|
|
│ email del docente │
|
|
└───────┬───────────────────────┘
|
|
↓
|
|
SI ↙ ↘ NO
|
|
↓
|
|
┌───────────────────┐
|
|
│ 5. Enviar email │
|
|
│ (MailService) │
|
|
└───────────────────┘
|
|
```
|
|
|
|
### Query SQL para Obtener Docente
|
|
|
|
```sql
|
|
SELECT DISTINCT ON (tc.teacher_id)
|
|
p.id as teacher_id,
|
|
p.display_name as teacher_name,
|
|
p.email as teacher_email,
|
|
p.preferences as teacher_preferences,
|
|
c.name as classroom_name
|
|
FROM social_features.classroom_members cm
|
|
JOIN social_features.teacher_classrooms tc ON tc.classroom_id = cm.classroom_id
|
|
JOIN auth_management.profiles p ON p.id = tc.teacher_id
|
|
JOIN social_features.classrooms c ON c.id = cm.classroom_id
|
|
WHERE cm.student_id = $1
|
|
AND cm.status = 'active'
|
|
AND cm.is_active = true
|
|
ORDER BY tc.teacher_id, tc.role = 'owner' DESC, tc.assigned_at ASC
|
|
LIMIT 1
|
|
```
|
|
|
|
**Lógica del Query:**
|
|
- Busca el aula (classroom) activa del estudiante
|
|
- Obtiene el teacher asignado al aula
|
|
- Prioriza `role = 'owner'` sobre otros roles
|
|
- Si hay múltiples teachers, toma el asignado primero (`assigned_at ASC`)
|
|
|
|
---
|
|
|
|
## Archivos Creados
|
|
|
|
### 1. `/apps/backend/src/modules/progress/events/exercise-submission.event.ts`
|
|
|
|
**Propósito:** Define el evento y payload para notificaciones de submissions.
|
|
|
|
```typescript
|
|
export interface ExerciseSubmissionEventPayload {
|
|
studentId: string;
|
|
studentName: string;
|
|
exerciseType: string;
|
|
exerciseTitle: string;
|
|
exerciseId: string;
|
|
submissionId: string;
|
|
moduleId: string;
|
|
moduleName?: string;
|
|
teacherId: string;
|
|
teacherEmail: string;
|
|
timestamp: Date;
|
|
}
|
|
|
|
export const EXERCISE_SUBMISSION_EVENT = 'student.exercise.submitted';
|
|
```
|
|
|
|
---
|
|
|
|
## Archivos Modificados
|
|
|
|
### 1. `/apps/backend/src/modules/progress/progress.module.ts`
|
|
|
|
**Cambios:**
|
|
- Agregado `import { NotificationsModule } from '../notifications/notifications.module'`
|
|
- Agregado `import { MailModule } from '../mail/mail.module'`
|
|
- Agregado en `imports`: `NotificationsModule` y `MailModule`
|
|
|
|
**Razón:** Permitir inyección de `NotificationsService` y `MailService` en `ExerciseSubmissionService`.
|
|
|
|
---
|
|
|
|
### 2. `/apps/backend/src/modules/progress/services/exercise-submission.service.ts`
|
|
|
|
**Cambios principales:**
|
|
|
|
#### A. Imports agregados
|
|
|
|
```typescript
|
|
import { NotificationsService } from '@/modules/notifications/services/notifications.service';
|
|
import { MailService } from '@/modules/mail/mail.service';
|
|
import { NotificationTypeEnum } from '@shared/constants/enums.constants';
|
|
```
|
|
|
|
#### B. Constructor actualizado
|
|
|
|
```typescript
|
|
constructor(
|
|
// ... otros parámetros ...
|
|
private readonly notificationsService: NotificationsService,
|
|
private readonly mailService: MailService,
|
|
) {}
|
|
```
|
|
|
|
#### C. Método `submitExercise()` modificado
|
|
|
|
**Ubicación:** Líneas 273-283
|
|
|
|
```typescript
|
|
// BE-P2-008: Notificar al docente si el ejercicio requiere revisión manual
|
|
if (exercise.requires_manual_grading) {
|
|
console.log(`[BE-P2-008] Exercise ${exerciseId} requires manual grading - notifying teacher`);
|
|
try {
|
|
await this.notifyTeacherOfSubmission(submission, exercise, profileId);
|
|
} catch (error) {
|
|
// Log error but don't fail submission
|
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
console.error(`[BE-P2-008] Failed to notify teacher: ${errorMessage}`);
|
|
}
|
|
}
|
|
```
|
|
|
|
**Lógica:**
|
|
- Verifica si `exercise.requires_manual_grading = true`
|
|
- Llama a `notifyTeacherOfSubmission()` de forma asíncrona
|
|
- **No bloquea** el submission si falla la notificación (try-catch)
|
|
|
|
#### D. Nuevo método `notifyTeacherOfSubmission()`
|
|
|
|
**Ubicación:** Líneas 1395-1524
|
|
|
|
**Responsabilidades:**
|
|
1. Obtener datos del estudiante (nombre, email)
|
|
2. Query SQL para obtener docente asignado
|
|
3. Enviar notificación in-app usando `NotificationsService`
|
|
4. Verificar preferencias del docente
|
|
5. Enviar email si está habilitado
|
|
|
|
**Notificación In-App:**
|
|
|
|
```typescript
|
|
await this.notificationsService.sendNotification({
|
|
userId: teacherId,
|
|
type: NotificationTypeEnum.EXERCISE_FEEDBACK,
|
|
title: 'Nuevo ejercicio para revisar',
|
|
message: `${studentName} ha enviado el ejercicio "${exercise.title}" (${exercise.exercise_type}) y está esperando tu revisión.`,
|
|
data: {
|
|
submissionId: submission.id,
|
|
exerciseId: exercise.id,
|
|
exerciseTitle: exercise.title,
|
|
exerciseType: exercise.exercise_type,
|
|
studentId: studentProfileId,
|
|
studentName: studentName,
|
|
reviewUrl: `/teacher/reviews/${submission.id}`,
|
|
classroomName: classroomName,
|
|
},
|
|
relatedEntityType: 'exercise_submission',
|
|
relatedEntityId: submission.id,
|
|
priority: 'high',
|
|
});
|
|
```
|
|
|
|
**Email:**
|
|
|
|
```typescript
|
|
const emailSubject = `Nuevo ejercicio para revisar: ${exercise.title}`;
|
|
const emailMessage = `
|
|
<p>Hola <strong>${teacherName}</strong>,</p>
|
|
<p><strong>${studentName}</strong> ha enviado el ejercicio <strong>"${exercise.title}"</strong> en ${classroomName}.</p>
|
|
<p><strong>Tipo de ejercicio:</strong> ${this.getExerciseTypeDisplayName(exercise.exercise_type)}</p>
|
|
<p>Este ejercicio requiere revisión manual. Por favor, revisa el trabajo del estudiante cuando tengas oportunidad.</p>
|
|
<p>Puedes acceder a la revisión desde tu panel de docente.</p>
|
|
`;
|
|
|
|
await this.mailService.sendNotificationEmail(
|
|
teacherEmail,
|
|
emailSubject,
|
|
emailMessage,
|
|
reviewUrl,
|
|
'Revisar ejercicio',
|
|
);
|
|
```
|
|
|
|
#### E. Nuevo método helper `getExerciseTypeDisplayName()`
|
|
|
|
**Ubicación:** Líneas 1532-1544
|
|
|
|
**Propósito:** Convertir códigos de ejercicios a nombres legibles en español.
|
|
|
|
```typescript
|
|
private getExerciseTypeDisplayName(exerciseType: string): string {
|
|
const displayNames: Record<string, string> = {
|
|
video_carta_curie: 'Video Carta Curie',
|
|
diario_multimedia: 'Diario Multimedia',
|
|
comic_digital: 'Cómic Digital',
|
|
podcast_argumentativo: 'Podcast Argumentativo',
|
|
debate_digital: 'Debate Digital',
|
|
infografia_interactiva: 'Infografía Interactiva',
|
|
};
|
|
|
|
return displayNames[exerciseType] || exerciseType;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Preferencias de Email
|
|
|
|
### Estructura de `profiles.preferences`
|
|
|
|
```json
|
|
{
|
|
"theme": "detective",
|
|
"language": "es",
|
|
"notifications_enabled": true,
|
|
"email_notifications": {
|
|
"exercise_feedback": true,
|
|
"achievement_unlocked": false,
|
|
"rank_up": true
|
|
}
|
|
}
|
|
```
|
|
|
|
### Verificación de Preferencias
|
|
|
|
```typescript
|
|
const emailNotificationsEnabled = teacherPreferences?.notifications_enabled !== false;
|
|
const exerciseFeedbackEmailEnabled = teacherPreferences?.email_notifications?.exercise_feedback !== false;
|
|
|
|
if (emailNotificationsEnabled && exerciseFeedbackEmailEnabled && teacherEmail) {
|
|
// Enviar email
|
|
}
|
|
```
|
|
|
|
**Lógica:**
|
|
- Si `notifications_enabled` no existe o es `true` → habilitar
|
|
- Si `email_notifications.exercise_feedback` no existe o es `true` → habilitar
|
|
- Requiere que `teacherEmail` esté presente
|
|
|
|
---
|
|
|
|
## Tipos de Ejercicios M4-M5
|
|
|
|
Ejercicios que requieren revisión manual (`requires_manual_grading = true`):
|
|
|
|
### Módulo 4 - Lectura Crítica e Investigación
|
|
- `video_carta_curie` - Video Carta Curie
|
|
- `podcast_argumentativo` - Podcast Argumentativo
|
|
- `debate_digital` - Debate Digital
|
|
- `infografia_interactiva` - Infografía Interactiva
|
|
|
|
### Módulo 5 - Lectura Multimodal y Digital
|
|
- `diario_multimedia` - Diario Multimedia
|
|
- `comic_digital` - Cómic Digital
|
|
|
|
**Nota:** Otros ejercicios con `auto_gradable = false` también podrían tener `requires_manual_grading = true` según la configuración.
|
|
|
|
---
|
|
|
|
## Logging y Debugging
|
|
|
|
Todos los logs usan el prefijo `[BE-P2-008]` para facilitar debugging:
|
|
|
|
```
|
|
[BE-P2-008] Exercise {exerciseId} requires manual grading - notifying teacher
|
|
[BE-P2-008] Notifying teacher about submission {submissionId}
|
|
[BE-P2-008] Found teacher {teacherId} ({teacherEmail}) for student {studentProfileId}
|
|
[BE-P2-008] ✅ In-app notification sent to teacher {teacherId}
|
|
[BE-P2-008] Email notifications enabled for teacher {teacherId} - sending email to {teacherEmail}
|
|
[BE-P2-008] ✅ Email notification sent to teacher {teacherEmail}
|
|
[BE-P2-008] ✅ Teacher notification process completed for submission {submissionId}
|
|
```
|
|
|
|
**Warnings:**
|
|
```
|
|
[BE-P2-008] Student profile {studentProfileId} not found - skipping notification
|
|
[BE-P2-008] No active teacher found for student {studentProfileId} - skipping notification
|
|
[BE-P2-008] Email notifications disabled for teacher {teacherId} - skipping email
|
|
[BE-P2-008] ⚠️ Email notification logged but not sent (SMTP not configured)
|
|
```
|
|
|
|
**Errores:**
|
|
```
|
|
[BE-P2-008] ❌ Failed to send in-app notification: {error}
|
|
[BE-P2-008] ❌ Failed to send email notification: {error}
|
|
[BE-P2-008] Failed to notify teacher: {error}
|
|
```
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
### Casos de Prueba Recomendados
|
|
|
|
#### 1. Submission de Ejercicio M4-M5 (Happy Path)
|
|
- **Setup:** Estudiante activo en classroom con teacher asignado
|
|
- **Acción:** Enviar submission de ejercicio `video_carta_curie`
|
|
- **Resultado esperado:**
|
|
- Submission se crea correctamente
|
|
- Notificación in-app se envía al teacher
|
|
- Email se envía si está habilitado en preferencias
|
|
|
|
#### 2. Estudiante sin Classroom Asignado
|
|
- **Setup:** Estudiante sin classroom activo
|
|
- **Acción:** Enviar submission de ejercicio M4
|
|
- **Resultado esperado:**
|
|
- Submission se crea correctamente
|
|
- Log de warning: "No active teacher found"
|
|
- No se envía notificación
|
|
|
|
#### 3. Preferencias de Email Deshabilitadas
|
|
- **Setup:** Teacher con `email_notifications.exercise_feedback = false`
|
|
- **Acción:** Enviar submission
|
|
- **Resultado esperado:**
|
|
- Notificación in-app se envía
|
|
- Email NO se envía
|
|
- Log: "Email notifications disabled"
|
|
|
|
#### 4. SMTP No Configurado
|
|
- **Setup:** Variables de entorno SMTP no configuradas
|
|
- **Acción:** Enviar submission
|
|
- **Resultado esperado:**
|
|
- Notificación in-app se envía
|
|
- Email se loggea pero no se envía
|
|
- Log: "Email notification logged but not sent"
|
|
|
|
#### 5. Ejercicio No Requiere Revisión Manual
|
|
- **Setup:** Ejercicio con `requires_manual_grading = false`
|
|
- **Acción:** Enviar submission
|
|
- **Resultado esperado:**
|
|
- Submission se procesa normalmente
|
|
- NO se envía notificación al teacher
|
|
- No hay logs de BE-P2-008
|
|
|
|
---
|
|
|
|
## Dependencias
|
|
|
|
### Servicios Externos
|
|
- `NotificationsService` (módulo `notifications`)
|
|
- `MailService` (módulo `mail`)
|
|
- `WebSocketService` (usado internamente por `NotificationsService`)
|
|
|
|
### Tablas de Base de Datos
|
|
- `progress_tracking.exercise_submissions`
|
|
- `educational_content.exercises`
|
|
- `auth_management.profiles`
|
|
- `social_features.classroom_members`
|
|
- `social_features.teacher_classrooms`
|
|
- `social_features.classrooms`
|
|
- `gamification_system.notifications`
|
|
|
|
---
|
|
|
|
## Variables de Entorno Requeridas
|
|
|
|
### Para Email (MailService)
|
|
```env
|
|
# Opción 1: SendGrid
|
|
SENDGRID_API_KEY=SG.xxxxxxxxxxxxx
|
|
|
|
# Opción 2: SMTP Genérico
|
|
SMTP_HOST=smtp.gmail.com
|
|
SMTP_PORT=587
|
|
SMTP_USER=your-email@gmail.com
|
|
SMTP_PASS=your-app-password
|
|
SMTP_SECURE=false
|
|
SMTP_FROM=GAMILIT <notifications@gamilit.com>
|
|
```
|
|
|
|
### Para Frontend URL
|
|
```env
|
|
FRONTEND_URL=http://localhost:3005
|
|
```
|
|
|
|
**Nota:** Si no hay configuración SMTP, los emails se loggean en consola pero no se envían.
|
|
|
|
---
|
|
|
|
## Limitaciones Conocidas
|
|
|
|
1. **Múltiples Teachers:** Si un classroom tiene múltiples teachers, solo se notifica al primero (según `role = 'owner'` o `assigned_at`).
|
|
- **Mejora futura:** Notificar a todos los teachers del aula.
|
|
|
|
2. **Eventos Asincrónicos:** Actualmente se llama directamente a `NotificationsService`. No usa sistema de eventos (EventEmitter).
|
|
- **Razón:** `@nestjs/event-emitter` no estaba instalado.
|
|
- **Mejora futura:** Migrar a eventos para desacoplamiento.
|
|
|
|
3. **Retry Logic:** Si falla el envío de notificación, NO se reintenta.
|
|
- **Mejora futura:** Usar `NotificationQueueService` para reintentospersistentes.
|
|
|
|
4. **Rate Limiting:** No hay límite de notificaciones por docente.
|
|
- **Mejora futura:** Implementar agrupación de notificaciones (digest).
|
|
|
|
---
|
|
|
|
## Seguridad
|
|
|
|
### Validaciones Implementadas
|
|
- ✅ Verificación de que el estudiante existe
|
|
- ✅ Verificación de que el classroom es activo (`status = 'active'`, `is_active = true`)
|
|
- ✅ Solo se notifica a teachers activos asignados al classroom
|
|
- ✅ Try-catch para evitar que errores bloqueen submissions
|
|
|
|
### Posibles Mejoras
|
|
- Agregar verificación de permisos (RLS policies)
|
|
- Validar que el ejercicio pertenece al módulo del curriculum del classroom
|
|
- Rate limiting por IP/usuario para prevenir spam
|
|
|
|
---
|
|
|
|
## Performance
|
|
|
|
### Impacto en Submission Flow
|
|
- **Query adicional:** 1 query SQL para obtener teacher (~10-50ms)
|
|
- **API call:** 1 llamada a `NotificationsService.sendNotification()` (~20-100ms)
|
|
- **Email:** 1 llamada a `MailService.sendNotificationEmail()` (asíncrono, ~100-500ms)
|
|
- **Total:** ~130-650ms adicionales al flujo de submission
|
|
|
|
**Optimizaciones implementadas:**
|
|
- Try-catch para no bloquear submission si falla notificación
|
|
- Email se envía solo si está habilitado en preferencias
|
|
- Query SQL optimizado con LIMIT 1 y índices en FK
|
|
|
|
### Mejoras Futuras
|
|
- Usar cola asíncrona (`NotificationQueueService`) para envío de emails
|
|
- Cache de classroom-teacher assignments para reducir queries
|
|
- Batch notifications si múltiples estudiantes envían al mismo tiempo
|
|
|
|
---
|
|
|
|
## Conclusión
|
|
|
|
La implementación de **BE-P2-008** está completa y cumple con todos los requisitos:
|
|
|
|
✅ Detección automática de ejercicios M4-M5
|
|
✅ Notificación in-app al docente
|
|
✅ Email opcional según preferencias
|
|
✅ Información completa (estudiante, ejercicio, enlace)
|
|
✅ Manejo robusto de errores
|
|
✅ Logging detallado para debugging
|
|
|
|
**Próximos pasos:**
|
|
1. Desplegar cambios a desarrollo
|
|
2. Testing manual con ejercicios M4-M5
|
|
3. Verificar logs en producción
|
|
4. Considerar mejoras futuras (eventos, cola, rate limiting)
|
|
|
|
---
|
|
|
|
**Documento generado:** 2025-12-05
|
|
**Implementado por:** Backend-Agent (Claude Sonnet 4.5)
|
|
**Revisado:** Pendiente
|