# 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 = `

Hola ${teacherName},

${studentName} ha enviado el ejercicio "${exercise.title}" en ${classroomName}.

Tipo de ejercicio: ${this.getExerciseTypeDisplayName(exercise.exercise_type)}

Este ejercicio requiere revisión manual. Por favor, revisa el trabajo del estudiante cuando tengas oportunidad.

Puedes acceder a la revisión desde tu panel de docente.

`; 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 = { 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 ``` ### 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