- 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>
7.8 KiB
7.8 KiB
US-LTI-002: Grade Passback (AGS)
Épica: EXT-007: LTI Integration Prioridad: P1 Story Points: 10 Esfuerzo: 10 horas Costo: $1,500 USD Sprint: 18
📋 User Story
Como profesor que asigna ejercicios GAMILITdesde Canvas,
Quiero que las calificaciones de mis estudiantes se sincronicen automáticamente
Para no tener que exportar/importar calificaciones manualmente
🎯 Contexto de Negocio
Problema Actual
- Profesores asignan actividades en LMS apuntando a GLIT
- Estudiantes completan ejercicios en GLIT
- Profesor debe exportar scores de GAMILIT→ importar a LMS manualmente
- Proceso toma 30+ minutos por clase
Solución
- Assignment & Grades Services (AGS): Estándar LTI 1.3 para sincronización de calificaciones
- GAMILITenvía scores automáticamente al LMS cuando estudiante completa ejercicio
- Sincronización bidireccional (LMS puede consultar scores)
Valor
- Time savings: 3h/semana por profesor (no sincronización manual)
- Accuracy: 100% vs 95% (eliminación de errores de transcripción)
- Real-time: Calificaciones aparecen en <10 segundos
- NPS profesores: +15 puntos
✅ Criterios de Aceptación
Funcionales
-
Resource Link Tracking:
- Cuando estudiante accede a ejercicio vía LTI, GAMILITalmacena:
line_item_url(endpoint para enviar scores)resource_id(ID del assignment en LMS)user_id+lms_user_id(mapeo)
- Información guardada en tabla
lti_resources
- Cuando estudiante accede a ejercicio vía LTI, GAMILITalmacena:
-
Score Submission (AGS):
- Cuando estudiante completa ejercicio:
- GAMILITcalcula score (0-100)
- Obtiene OAuth2 token del LMS (client credentials flow)
- POST a
line_item_url/scorescon payload:{ "userId": "lms_user_id", "scoreGiven": 85, "scoreMaximum": 100, "activityProgress": "Completed", "gradingProgress": "FullyGraded", "timestamp": "2025-11-07T10:30:00Z" }
- Score aparece en gradebook del LMS
- Cuando estudiante completa ejercicio:
-
Multiple Attempts Handling:
- Si estudiante reintenta ejercicio:
- GAMILITenvía nuevo score solo si es mayor que anterior
activityProgressse mantiene "Completed"
- Política configurable: "highest", "latest", "average"
- Si estudiante reintenta ejercicio:
-
Error Handling:
- Si POST falla (network, 4xx, 5xx):
- Guardar en
lti_grade_passback_logcon status "failed" - Retry automático: 3 intentos con exponential backoff (1s, 5s, 15s)
- Si fallan todos → notificar admin vía email
- Guardar en
- Success: Guardar en log con status "success"
- Si POST falla (network, 4xx, 5xx):
-
OAuth2 Token Management:
- Obtener access token del LMS usando client credentials
- Cachear token en Redis (expira en 3600s - 10% margin = 3540s)
- Si token expirado/inválido → renovar automáticamente
No Funcionales
-
Performance:
- Grade passback <10 segundos desde completion
- Soporta 50 submissions concurrentes
-
Reliability:
- Success rate >98%
- Retry mechanism robusto
- Idempotencia (mismo score enviado múltiples veces = 1 resultado)
-
Monitoring:
- Metrics: submission rate, success rate, average latency
- Alertas si success rate <95%
🔧 Tareas Técnicas
Backend (9h)
-
OAuth2 Client Credentials (2h)
- Service para obtener access token del LMS
- POST a
auth_token_urlcon:grant_type=client_credentials client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer client_assertion=<JWT firmado con private key> scope=https://purl.imsglobal.org/spec/lti-ags/scope/score - Parse response y cachear token
-
AGS Score Submission (3h)
- Service
submitGradeToLMS(attemptId: string) - Construir payload AGS conforme a spec
- HTTP client con retry logic (axios-retry)
- Validación de response (200 OK vs 4xx/5xx)
- Service
-
Line Item Discovery (1h)
- Al recibir launch LTI, extraer claims:
https://purl.imsglobal.org/spec/lti-ags/claim/endpointlineitem(URL del assignment)
- Guardar en
lti_resources
- Al recibir launch LTI, extraer claims:
-
Integration con Exercise Completion (1h)
- Hook en
ExerciseService.completeAttempt() - Si attempt tiene
lti_resource_id:- Trigger grade passback async (queue)
- No bloquear completion si LMS falla
- Hook en
-
Retry Queue (1h)
- Bull queue:
lti-grade-passback - Worker procesa jobs con retry exponential backoff
- Dead letter queue para jobs fallidos permanentemente
- Bull queue:
-
Testing (1h)
- Unit tests de OAuth2 flow
- Integration test de AGS submission (mock LMS)
- Test de retry logic
Admin UI (1h)
- Grade Passback Log Viewer (1h)
- Tabla en Admin Portal: recent grade submissions
- Columnas: student, exercise, score, status, timestamp, error
- Filtro por status (success/failed)
- Botón "Retry Failed" para manualmente reintentar
🧪 Escenarios de Testing
Happy Path
Given: Estudiante completa ejercicio asignado vía Canvas LTI
When: Score calculado = 92/100
Then:
- OAuth2 token obtenido del LMS
- Score enviado a Canvas AGS endpoint
- Response 200 OK
- Canvas gradebook muestra 92%
- Log entry creado con status "success"
Edge Cases
-
LMS temporalmente down:
When: Grade passback request falla con 503 Then: - Retry después de 1s → falla - Retry después de 5s → falla - Retry después de 15s → success - Score finalmente enviado -
Student intenta 3 veces:
Given: Attempt 1 = 70%, Attempt 2 = 85%, Attempt 3 = 75% When: Policy = "highest" Then: - Attempt 1: envía 70 - Attempt 2: envía 85 (mayor) - Attempt 3: NO envía (75 < 85) - LMS muestra 85% -
OAuth token expirado:
Given: Token cacheado expiró When: Intentar enviar score Then: - Detecta 401 Unauthorized - Renueva token automáticamente - Reintenta submission → success
📊 Métricas de Éxito
Durante Desarrollo
- Tests passing: 100%
- Code coverage: >80%
Post-Lanzamiento (1 mes)
- Grade passback success rate: >98%
- Average latency: <5 segundos
- Retry rate: <5%
- Professor satisfaction: NPS >70
🔗 Dependencias
Bloqueado por
- US-LTI-001: OIDC Login (usuarios autenticados vía LTI)
- Exercise completion backend funcionando
Bloquea
- EXT-001: Portal Maestros Completo (dashboard de calificaciones)
📚 Referencias Técnicas
Specification
Código de Referencia
// Service
async submitGradeToLMS(attemptId: string) {
const attempt = await this.exerciseService.getAttempt(attemptId);
const ltiContext = await this.ltiService.getLTIContext(attempt.user_id);
if (!ltiContext?.line_item_url) return; // Not an LTI launch
// Get OAuth2 token
const accessToken = await this.getAccessToken(ltiContext.platform_id);
// Construct AGS payload
const payload = {
userId: ltiContext.lms_user_id,
scoreGiven: attempt.score,
scoreMaximum: 100,
activityProgress: attempt.is_completed ? 'Completed' : 'InProgress',
gradingProgress: 'FullyGraded',
timestamp: new Date().toISOString()
};
// Submit to LMS
try {
await axios.post(`${ltiContext.line_item_url}/scores`, payload, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
await this.logGradePassback(attemptId, 'success');
} catch (error) {
await this.logGradePassback(attemptId, 'failed', error.message);
throw error; // Trigger retry
}
}
Creado: 2025-11-07 Asignado a: Backend Team Revisor: Tech Lead