# πŸ”„ Diagrama de Flujo: Submissions de Ejercicios **Fecha:** 2025-11-19 **VersiΓ³n:** 1.0 **Autor:** Database Agent --- ## 🎯 Flujo Completo: Usuario Completa Ejercicio ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ FRONTEND LAYER β”‚ β”‚ (React + TypeScript + TanStack Query) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 1. User completa ejercicio β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Componente de Ejercicio (ej: CrucigramaExercise.tsx) β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ Estado local: β”‚ β”‚ { β”‚ β”‚ clues: { β”‚ β”‚ h1: {userInput: "SORBONA"}, β”‚ β”‚ h2: {userInput: "NOBEL"} β”‚ β”‚ } β”‚ β”‚ } β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 2. User hace clic "Enviar" β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ValidaciΓ³n Client-Side (UI) β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ βœ“ Campos requeridos completos β”‚ β”‚ βœ“ Formato bΓ‘sico correcto β”‚ β”‚ βœ— NO valida correcciΓ³n (nunca tiene respuestas correctas) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 3. Transform to submission format β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ TransformaciΓ³n a formato API β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ Frontend State β†’ API Request: β”‚ β”‚ β”‚ β”‚ { β”‚ β”‚ exerciseId: "uuid-here", β”‚ β”‚ answers: { β”‚ β”‚ clues: { β”‚ β”‚ h1: "SORBONA", β”‚ β”‚ h2: "NOBEL" β”‚ β”‚ } β”‚ β”‚ } β”‚ β”‚ } β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 4. HTTP POST Request β–Ό ═════════════════════════════════════════════════════════════════════════ HTTP BOUNDARY (HTTPS/TLS) ═════════════════════════════════════════════════════════════════════════ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ BACKEND LAYER β”‚ β”‚ (NestJS + TypeScript + TypeORM) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 5. ExercisesController recibe request β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ POST /api/exercises/:id/submit β”‚ β”‚ Controller: exercises.controller.ts β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ @Post(':id/submit') β”‚ β”‚ async submitExercise( β”‚ β”‚ @Param('id') exerciseId: string, β”‚ β”‚ @Body() submission: SubmitExerciseDto, β”‚ β”‚ @Request() req β”‚ β”‚ ) { β”‚ β”‚ const userId = req.user.id; // From JWT β”‚ β”‚ return this.submissionService.submitAndGrade( β”‚ β”‚ userId, exerciseId, submission.answers β”‚ β”‚ ); β”‚ β”‚ } β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 6. ExerciseSubmissionService.submitAndGrade() β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Step 1: ConversiΓ³n auth.users.id β†’ profiles.id β”‚ β”‚ Service: exercise-submission.service.ts:45-56 β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ const profileId = await this.getProfileId(userId); β”‚ β”‚ β”‚ β”‚ // CRITICAL FIX: exercise_submissions.user_id references profiles.id β”‚ β”‚ // JWT tiene auth.users.id, necesitamos convertir β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 7. Validar estructura de respuesta β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Step 2: ExerciseAnswerValidator.validate() β”‚ β”‚ File: dto/answers/exercise-answer.validator.ts β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ 1. Get exercise type β†’ "crucigrama" β”‚ β”‚ 2. Map to DTO β†’ CrucigramaAnswersDto β”‚ β”‚ 3. Validate structure with class-validator β”‚ β”‚ β”‚ β”‚ IF invalid structure: β”‚ β”‚ β†’ throw BadRequestException β”‚ β”‚ β†’ Frontend recibe 400 con detalles β”‚ β”‚ β”‚ β”‚ IF valid structure: β”‚ β”‚ β†’ Continue βœ“ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 8. Crear submission record β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Step 3: Create ExerciseSubmission β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ const submissionData = { β”‚ β”‚ user_id: profileId, // profiles.id (NO auth.users.id) β”‚ β”‚ exercise_id: exerciseId, β”‚ β”‚ answer_data: answers, β”‚ β”‚ max_score: 100, β”‚ β”‚ attempt_number: 1, β”‚ β”‚ status: 'submitted' β”‚ β”‚ }; β”‚ β”‚ β”‚ β”‚ const submission = await this.submissionRepo.save(submissionData); β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 9. Auto-grading β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Step 4: autoGrade() β†’ SQL validate_and_audit() β”‚ β”‚ Service: exercise-submission.service.ts:251-309 β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ const result = await this.entityManager.query(` β”‚ β”‚ SELECT * FROM educational_content.validate_and_audit( β”‚ β”‚ $1::uuid, -- exercise_id β”‚ β”‚ $2::uuid, -- user_id (profiles.id) β”‚ β”‚ $3::jsonb, -- submitted_answer β”‚ β”‚ $4::integer, -- attempt_number β”‚ β”‚ $5::jsonb -- client_metadata β”‚ β”‚ ) β”‚ β”‚ `, [exerciseId, profileId, answers, 1, {}]); β”‚ β”‚ β”‚ β”‚ return { β”‚ β”‚ score: result[0].score, β”‚ β”‚ isCorrect: result[0].is_correct, β”‚ β”‚ correctAnswers: result[0].details?.correct_answers, β”‚ β”‚ totalQuestions: result[0].details?.total_questions, β”‚ β”‚ feedback: result[0].feedback, β”‚ β”‚ details: result[0].details, β”‚ β”‚ auditId: result[0].audit_id β”‚ β”‚ }; β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ ═════════════════════════════════════════════════════════════════════════ DATABASE BOUNDARY (PostgreSQL) ═════════════════════════════════════════════════════════════════════════ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ DATABASE LAYER β”‚ β”‚ (PostgreSQL 15 + PL/pgSQL Functions) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 10. validate_and_audit() comienza β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Function: validate_and_audit() β”‚ β”‚ File: 20-validate_and_audit.sql β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ Step 1: Recuperar ejercicio y configuraciΓ³n β”‚ β”‚ β”‚ β”‚ SELECT exercise_type, content, solution, max_points β”‚ β”‚ FROM educational_content.exercises β”‚ β”‚ WHERE id = p_exercise_id AND is_active = true; β”‚ β”‚ β”‚ β”‚ β†’ exercise_type = 'crucigrama' β”‚ β”‚ β”‚ β”‚ SELECT validation_function, case_sensitive, ... β”‚ β”‚ FROM educational_content.exercise_validation_config β”‚ β”‚ WHERE exercise_type = 'crucigrama'; β”‚ β”‚ β”‚ β”‚ β†’ validation_function = 'validate_crucigrama' β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 11. Despachar a validador especΓ­fico β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Function: validate_answer() (Dispatcher) β”‚ β”‚ File: 02-validate_answer.sql β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ CASE v_config.validation_function β”‚ β”‚ WHEN 'validate_crucigrama' THEN β”‚ β”‚ SELECT * FROM educational_content.validate_crucigrama( β”‚ β”‚ v_exercise.solution, -- Respuestas correctas β”‚ β”‚ p_submitted_answer, -- Respuestas del usuario β”‚ β”‚ max_score, -- 100 β”‚ β”‚ v_config.case_sensitive, -- false β”‚ β”‚ v_config.normalize_text -- true β”‚ β”‚ ); β”‚ β”‚ END CASE; β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 12. Validar respuestas β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Function: validate_crucigrama() β”‚ β”‚ File: 03-validate_crucigrama.sql β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ Inputs: β”‚ β”‚ β€’ solution: {"clues": {"h1": "SORBONA", "h2": "NOBEL"}} β”‚ β”‚ β€’ submitted: {"clues": {"h1": "sorbona", "h2": "nobel"}} β”‚ β”‚ β”‚ β”‚ Process: β”‚ β”‚ 1. FOR EACH clue_id IN solution.clues: β”‚ β”‚ a. Normalize: UPPER(TRIM(normalize_text(answer))) β”‚ β”‚ b. Compare: submitted[clue_id] == solution[clue_id] β”‚ β”‚ c. Count correct_words++ β”‚ β”‚ β”‚ β”‚ 2. Calculate: β”‚ β”‚ score = (correct_words / total_words) * max_points β”‚ β”‚ is_correct = (correct_words == total_words) β”‚ β”‚ β”‚ β”‚ 3. Generate feedback: β”‚ β”‚ "Β‘Perfecto! Todas las 2 palabras estΓ‘n correctas." β”‚ β”‚ β”‚ β”‚ 4. Build details JSONB: β”‚ β”‚ { β”‚ β”‚ "total_words": 2, β”‚ β”‚ "correct_words": 2, β”‚ β”‚ "percentage": 100, β”‚ β”‚ "results_per_word": [ β”‚ β”‚ {"clue_id": "h1", "is_correct": true, ...}, β”‚ β”‚ {"clue_id": "h2", "is_correct": true, ...} β”‚ β”‚ ] β”‚ β”‚ } β”‚ β”‚ β”‚ β”‚ Return: (is_correct, score, feedback, details) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 13. Crear auditorΓ­a β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Back in validate_and_audit(): Create Audit Record β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ INSERT INTO educational_content.exercise_validation_audit ( β”‚ β”‚ exercise_id, β”‚ β”‚ user_id, β”‚ β”‚ attempt_number, β”‚ β”‚ submitted_answer, -- Snapshot de respuesta β”‚ β”‚ exercise_snapshot, -- Snapshot del ejercicio β”‚ β”‚ validation_config_snapshot, -- Snapshot de config β”‚ β”‚ is_correct, β”‚ β”‚ score, β”‚ β”‚ max_score, β”‚ β”‚ feedback, β”‚ β”‚ validation_details, β”‚ β”‚ validation_function_used, -- 'validate_crucigrama' β”‚ β”‚ validation_timestamp, β”‚ β”‚ validation_duration_ms, -- Ej: 15ms β”‚ β”‚ client_metadata -- IP, user-agent, etc. β”‚ β”‚ ) β”‚ β”‚ RETURNING id INTO audit_id; β”‚ β”‚ β”‚ β”‚ audit_id = "550e8400-e29b-41d4-a716-446655440000" β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 14. Return result β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ validate_and_audit() returns RECORD: β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ { β”‚ β”‚ is_correct: true, β”‚ β”‚ score: 100, β”‚ β”‚ max_score: 100, β”‚ β”‚ feedback: "Β‘Perfecto! Todas las 2 palabras estΓ‘n correctas.", β”‚ β”‚ details: { β”‚ β”‚ total_words: 2, β”‚ β”‚ correct_words: 2, β”‚ β”‚ percentage: 100, β”‚ β”‚ results_per_word: [...] β”‚ β”‚ }, β”‚ β”‚ audit_id: "550e8400-..." β”‚ β”‚ } β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ ═════════════════════════════════════════════════════════════════════════ BACK TO BACKEND ═════════════════════════════════════════════════════════════════════════ β”‚ β”‚ 15. Process result β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ExerciseSubmissionService: Update submission β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ submission.score = 100; β”‚ β”‚ submission.is_correct = true; β”‚ β”‚ submission.status = 'graded'; β”‚ β”‚ submission.graded_at = new Date(); β”‚ β”‚ submission.feedback = "Β‘Perfecto!..."; β”‚ β”‚ β”‚ β”‚ await this.submissionRepo.save(submission); β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ 16. Transform to API response β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Response to Frontend β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ return { β”‚ β”‚ submissionId: submission.id, β”‚ β”‚ score: 100, β”‚ β”‚ maxScore: 100, β”‚ β”‚ isCorrect: true, β”‚ β”‚ feedback: "Β‘Perfecto! Todas las 2 palabras estΓ‘n correctas.", β”‚ β”‚ correctAnswers: 2, β”‚ β”‚ totalQuestions: 2, β”‚ β”‚ xpEarned: 20, // From exercise config β”‚ β”‚ mlCoinsEarned: 10 // From exercise config β”‚ β”‚ }; β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ ═════════════════════════════════════════════════════════════════════════ BACK TO FRONTEND ═════════════════════════════════════════════════════════════════════════ β”‚ β”‚ 17. Handle response β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Frontend: Process submission result β”‚ β”‚ ────────────────────────────────────────────────────────────────────│ β”‚ 1. Update local state β”‚ β”‚ 2. Show success/error feedback to user β”‚ β”‚ 3. Animate XP/ML Coins earned β”‚ β”‚ 4. Update user stats (if correct) β”‚ β”‚ 5. Enable "Continuar" button β”‚ β”‚ 6. Invalidate queries to refresh dashboard β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό USER SEES RESULT ``` --- ## πŸ”„ Transformaciones de Datos ### TransformaciΓ³n 1: Frontend State β†’ API Request **Input (React State):** ```typescript const [userAnswers, setUserAnswers] = useState({ h1: { value: "SORBONA", isConfirmed: true }, h2: { value: "NOBEL", isConfirmed: true } }); ``` **Output (API Request):** ```typescript POST /api/exercises/uuid-here/submit { "answers": { "clues": { "h1": "SORBONA", "h2": "NOBEL" } } } ``` --- ### TransformaciΓ³n 2: auth.users.id β†’ profiles.id **Input (JWT token):** ```typescript { sub: "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", // auth.users.id email: "user@example.com", role: "student" } ``` **Process:** ```typescript const profile = await this.profileRepo.findOne({ where: { user_id: userId }, select: ['id'] }); ``` **Output:** ```typescript profileId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" // profiles.id ``` --- ### TransformaciΓ³n 3: Backend Request β†’ SQL Parameters **Input (Backend):** ```typescript autoGrade( profileId: "bbbbbbbb-...", exerciseId: "cccccccc-...", answerData: {"clues": {"h1": "SORBONA", "h2": "NOBEL"}}, attemptNumber: 1, clientMetadata: {} ) ``` **SQL Call:** ```sql SELECT * FROM educational_content.validate_and_audit( 'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid, 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid, '{"clues":{"h1":"SORBONA","h2":"NOBEL"}}'::jsonb, 1::integer, '{}'::jsonb ); ``` --- ### TransformaciΓ³n 4: SQL Result β†’ Backend Response **SQL Output:** ```sql is_correct | score | max_score | feedback | details | audit_id ------------|-------|-----------|---------------------------------------|--------------------------|---------- true | 100 | 100 | Β‘Perfecto! Todas las 2 palabras... | {"total_words": 2, ...} | uuid-here ``` **Backend Response:** ```typescript { score: 100, isCorrect: true, correctAnswers: 2, totalQuestions: 2, feedback: "Β‘Perfecto! Todas las 2 palabras estΓ‘n correctas.", details: { total_words: 2, correct_words: 2, percentage: 100, results_per_word: [...] }, auditId: "550e8400-e29b-41d4-a716-446655440000" } ``` --- ## πŸ”’ Puntos de ValidaciΓ³n | # | Capa | Tipo de ValidaciΓ³n | Si Falla | |---|------|--------------------|----------| | **1** | Frontend UI | Campos requeridos, formato bΓ‘sico | BotΓ³n "Enviar" deshabilitado | | **2** | Backend DTO | Estructura de datos (class-validator) | `400 Bad Request` | | **3** | Backend Auth | Usuario autenticado, permisos | `401 Unauthorized` / `403 Forbidden` | | **4** | Backend Logic | auth.users.id β†’ profiles.id conversion | `404 Not Found` (profile not found) | | **5** | Database SQL | CorrecciΓ³n de respuestas | Score = 0-100, is_correct = false | --- ## πŸ“ Casos Edge ### Caso 1: Usuario envΓ­a respuesta sin autenticar ``` Frontend β†’ POST /api/exercises/:id/submit (Sin header Authorization) Backend β†’ Guard rechaza request β†’ 401 Unauthorized Frontend ← "No autorizado. Por favor inicia sesiΓ³n." ``` --- ### Caso 2: Estructura de respuesta invΓ‘lida ``` Frontend β†’ POST /api/exercises/:id/submit { "answers": { "invalid_key": "data" // Falta "clues" } } Backend β†’ ExerciseAnswerValidator.validate('crucigrama', answers) β†’ ValidationError: "clues is required" β†’ 400 Bad Request Frontend ← { "statusCode": 400, "message": "Validation failed for exercise type 'crucigrama': clues is required" } ``` --- ### Caso 3: Ejercicio no existe o inactivo ``` Frontend β†’ POST /api/exercises/invalid-uuid/submit Backend β†’ autoGrade() calls validate_and_audit() Database β†’ SELECT ... WHERE id = 'invalid-uuid' AND is_active = true; β†’ NOT FOUND β†’ RAISE EXCEPTION 'Exercise not found or inactive' Backend ← InternalServerErrorException β†’ 500 Internal Server Error Frontend ← "Error al validar. Contacte al administrador." ``` --- ### Caso 4: Usuario sin profile (datos inconsistentes) ``` Backend β†’ getProfileId(userId) β†’ SELECT ... FROM profiles WHERE user_id = 'user-id' β†’ NOT FOUND β†’ throw NotFoundException('Profile not found for user') β†’ 404 Not Found Frontend ← "Perfil no encontrado. Contacte soporte." ``` --- ## ⏱️ Performance ### MΓ©tricas TΓ­picas | Fase | Tiempo tΓ­pico | Notas | |------|---------------|-------| | Frontend validation | < 1ms | SΓ­ncrono, local | | HTTP request | 10-50ms | Depende de latencia red | | Backend DTO validation | 1-5ms | class-validator | | SQL validate_and_audit() | 5-20ms | Incluye auditorΓ­a | | Database commit | 1-5ms | Write to disk | | HTTP response | 10-50ms | Latencia red | | **Total (typical)** | **50-150ms** | E2E time | **Optimizaciones aplicadas:** - βœ… ValidaciΓ³n client-side reduce requests invΓ‘lidos - βœ… Índices en `exercises.id`, `profiles.user_id` - βœ… FunciΓ³n SQL `IMMUTABLE` cuando posible - βœ… AuditorΓ­a asΓ­ncrona (no bloquea respuesta) --- ## 🎯 Conclusiones 1. **Flujo bien definido:** 3 capas claramente separadas 2. **Validaciones defensivas:** 5 puntos de validaciΓ³n 3. **Trazabilidad completa:** AuditorΓ­a de cada submission 4. **Seguridad por capas:** Respuestas correctas NUNCA llegan a frontend 5. **Performance aceptable:** 50-150ms E2E tΓ­pico --- **Última actualizaciΓ³n:** 2025-11-19 **Ver tambiΓ©n:** ESPECIFICACION-VALIDACIONES-POR-TIPO-2025-11-19.md