From 289c5a4ee5b2c781b9cc36ad385d81bb19efa52c Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Thu, 18 Dec 2025 23:42:48 -0600 Subject: [PATCH] Gamilit: Backend fixes, frontend API updates, deployment guides and validations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - Fix email verification and password recovery services - Fix exercise submission and student progress services Frontend: - Update missions, password, and profile API services - Fix ExerciseContentRenderer component Docs & Scripts: - Add SSL/Certbot deployment guide - Add quick deployment guide - Database scripts for testing and validations - Migration and homologation reports - Functions inventory documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../services/email-verification.service.ts | 22 +- .../services/password-recovery.service.ts | 32 +- .../services/exercise-submission.service.ts | 122 +- .../services/student-progress.service.ts | 2 +- .../gamilit/apps/database/scripts/INDEX.md | 396 ++++++ .../apps/database/scripts/QUICK-START.md | 317 +++++ .../testing/CREAR-USUARIOS-TESTING.sql | 395 ++++++ .../database/scripts/validations/README.md | 205 +++ .../VALIDACIONES-RAPIDAS-POST-RECREACION.sql | 219 +++ .../validations/validate-gap-fixes.sql | 234 ++++ .../validate-generate-alerts-joins.sql | 253 ++++ ...validate-missions-objectives-structure.sql | 135 ++ .../validations/validate-seeds-integrity.sql | 248 ++++ .../validate-update-user-rank-fix.sql | 231 ++++ .../validate-user-initialization.sql | 499 +++++++ .../scripts/validations/validate_integrity.py | 474 +++++++ .../frontend/src/services/api/missionsAPI.ts | 49 +- .../frontend/src/services/api/passwordAPI.ts | 23 +- .../frontend/src/services/api/profileAPI.ts | 49 +- .../mechanics/ExerciseContentRenderer.tsx | 231 +++- .../ET-GAM-003-rangos-maya.md | 193 ++- .../ET-AUD-001-sistema-auditoria.md | 131 ++ .../ET-HLT-001-health-checks.md | 134 ++ .../especificaciones/ET-TSK-001-cron-jobs.md | 194 +++ .../especificaciones/ET-WS-001-websocket.md | 223 +++ .../inventarios/04-FUNCTIONS-INVENTORY.md | 304 +++++ .../migraciones/MIGRACION-MAYA-RANKS-v2.1.md | 363 +++++ .../GUIA-DEPLOYMENT-RAPIDO.md | 227 +++ .../GUIA-SSL-CERTBOT-DEPLOYMENT.md | 356 +++++ .../functions/VALIDATE-RUEDA-INFERENCIAS.md | 292 ++++ .../ALERT-COMPONENTS-ARCHITECTURE.md | 438 ++++++ .../admin/hooks/ADMIN-CLASSROOMS-HOOK.md | 165 +++ .../hooks/ADMIN-GAMIFICATION-CONFIG-HOOK.md | 345 +++++ .../pages/AdminAlertsPage-Specification.md | 199 +++ .../AdminGamificationPage-Specification.md | 202 +++ .../pages/AdminUsersPage-Specification.md | 212 +++ .../guides/Frontend-Alert-System-Guide.md | 362 +++++ .../TEACHER-MONITORING-COMPONENTS.md | 368 +++++ .../components/TEACHER-RESPONSE-MANAGEMENT.md | 346 +++++ .../pages/TEACHER-PAGES-SPECIFICATIONS.md | 290 ++++ .../teacher/types/TEACHER-TYPES-REFERENCE.md | 409 ++++++ .../CONSOLIDADO-HALLAZGOS.md | 220 +++ .../PLAN-ANALISIS-BACKEND.md | 153 +++ .../PLAN-IMPLEMENTACIONES.md | 355 +++++ .../FASE1-PLAN-ANALISIS.md | 289 ++++ .../FASE2-REPORTE-CONSOLIDADO.md | 339 +++++ .../FASE3-PLAN-IMPLEMENTACIONES.md | 539 ++++++++ .../FASE4-VALIDACION-DEPENDENCIAS.md | 279 ++++ .../FASE5-REPORTE-IMPLEMENTACIONES.md | 334 +++++ .../EJECUTAR-AQUI.md | 127 ++ .../FASE4-VALIDACION-DEPENDENCIAS.md | 373 +++++ .../FASE5-REPORTE-IMPLEMENTACION.md | 264 ++++ .../HALLAZGOS-RESUMEN.md | 322 +++++ .../INDEX.md | 320 +++++ .../INSTRUCCIONES-EJECUCION.md | 25 + .../PLAN-ANALISIS-HOMOLOGACION.md | 134 ++ .../README.md | 217 +++ .../REPORTE-DDL-DIFERENCIAS.md | 690 ++++++++++ .../REPORTE-FINAL-CONSOLIDADO.md | 418 ++++++ .../REPORTE-SCRIPTS-DIFERENCIAS.md | 1212 +++++++++++++++++ .../REPORTE-SCRIPTS-PRINCIPALES.md | 468 +++++++ .../REPORTE-SEEDS-DIFERENCIAS.md | 680 +++++++++ .../RESUMEN-EJECUTIVO.md | 236 ++++ .../START-HERE.md | 298 ++++ .../analyze_direct.py | 443 ++++++ .../compare-ddl.sh | 84 ++ .../compare_ddl.py | 247 ++++ .../migrate-scripts.sh | 511 +++++++ .../quick-summary.sh | 55 + .../run_comparison.sh | 3 + .../FASE1-ANALISIS-DIFERENCIAS.md | 330 +++++ .../FASE2-ANALISIS-DETALLADO.md | 261 ++++ .../FASE3-PLAN-IMPLEMENTACION.md | 556 ++++++++ .../FASE4-VALIDACION-DEPENDENCIAS.md | 360 +++++ .../FASE1-ANALISIS-CAMBIOS-CODIGO-VS-DOCS.md | 151 ++ .../FASE1-ANALISIS-DIFERENCIAS-DOCS.md | 86 ++ .../FASE2-ANALISIS-COMPLETO-CONSOLIDADO.md | 219 +++ ...FASE3-PLAN-IMPLEMENTACION-DOCUMENTACION.md | 367 +++++ .../FASE4-VALIDACION-PLAN-IMPLEMENTACION.md | 241 ++++ .../FASE5-REPORTE-FINAL-DOCUMENTACION.md | 182 +++ ...ANALISIS-PRODUCCION-COMPLETO-2025-12-18.md | 868 ++++++++++++ ...EPORTE-HOMOLOGACION-DATABASE-2025-12-18.md | 165 +++ ...HOMOLOGACION-DOCS-DESARROLLO-2025-12-18.md | 167 +++ projects/gamilit/scripts/README.md | 6 + projects/gamilit/scripts/setup-ssl-certbot.sh | 482 +++++++ .../gamilit/scripts/validate-deployment.sh | 465 +++++++ 86 files changed, 24223 insertions(+), 207 deletions(-) create mode 100644 projects/gamilit/apps/database/scripts/INDEX.md create mode 100644 projects/gamilit/apps/database/scripts/QUICK-START.md create mode 100644 projects/gamilit/apps/database/scripts/testing/CREAR-USUARIOS-TESTING.sql create mode 100644 projects/gamilit/apps/database/scripts/validations/README.md create mode 100644 projects/gamilit/apps/database/scripts/validations/VALIDACIONES-RAPIDAS-POST-RECREACION.sql create mode 100644 projects/gamilit/apps/database/scripts/validations/validate-gap-fixes.sql create mode 100644 projects/gamilit/apps/database/scripts/validations/validate-generate-alerts-joins.sql create mode 100644 projects/gamilit/apps/database/scripts/validations/validate-missions-objectives-structure.sql create mode 100644 projects/gamilit/apps/database/scripts/validations/validate-seeds-integrity.sql create mode 100644 projects/gamilit/apps/database/scripts/validations/validate-update-user-rank-fix.sql create mode 100644 projects/gamilit/apps/database/scripts/validations/validate-user-initialization.sql create mode 100644 projects/gamilit/apps/database/scripts/validations/validate_integrity.py create mode 100644 projects/gamilit/docs/90-transversal/arquitectura/especificaciones/ET-AUD-001-sistema-auditoria.md create mode 100644 projects/gamilit/docs/90-transversal/arquitectura/especificaciones/ET-HLT-001-health-checks.md create mode 100644 projects/gamilit/docs/90-transversal/arquitectura/especificaciones/ET-TSK-001-cron-jobs.md create mode 100644 projects/gamilit/docs/90-transversal/arquitectura/especificaciones/ET-WS-001-websocket.md create mode 100644 projects/gamilit/docs/90-transversal/inventarios-database/inventarios/04-FUNCTIONS-INVENTORY.md create mode 100644 projects/gamilit/docs/90-transversal/migraciones/MIGRACION-MAYA-RANKS-v2.1.md create mode 100644 projects/gamilit/docs/95-guias-desarrollo/GUIA-DEPLOYMENT-RAPIDO.md create mode 100644 projects/gamilit/docs/95-guias-desarrollo/GUIA-SSL-CERTBOT-DEPLOYMENT.md create mode 100644 projects/gamilit/docs/database/functions/VALIDATE-RUEDA-INFERENCIAS.md create mode 100644 projects/gamilit/docs/frontend/admin/components/ALERT-COMPONENTS-ARCHITECTURE.md create mode 100644 projects/gamilit/docs/frontend/admin/hooks/ADMIN-CLASSROOMS-HOOK.md create mode 100644 projects/gamilit/docs/frontend/admin/hooks/ADMIN-GAMIFICATION-CONFIG-HOOK.md create mode 100644 projects/gamilit/docs/frontend/admin/pages/AdminAlertsPage-Specification.md create mode 100644 projects/gamilit/docs/frontend/admin/pages/AdminGamificationPage-Specification.md create mode 100644 projects/gamilit/docs/frontend/admin/pages/AdminUsersPage-Specification.md create mode 100644 projects/gamilit/docs/frontend/guides/Frontend-Alert-System-Guide.md create mode 100644 projects/gamilit/docs/frontend/teacher/components/TEACHER-MONITORING-COMPONENTS.md create mode 100644 projects/gamilit/docs/frontend/teacher/components/TEACHER-RESPONSE-MANAGEMENT.md create mode 100644 projects/gamilit/docs/frontend/teacher/pages/TEACHER-PAGES-SPECIFICATIONS.md create mode 100644 projects/gamilit/docs/frontend/teacher/types/TEACHER-TYPES-REFERENCE.md create mode 100644 projects/gamilit/orchestration/analisis-backend-2025-12-18/CONSOLIDADO-HALLAZGOS.md create mode 100644 projects/gamilit/orchestration/analisis-backend-2025-12-18/PLAN-ANALISIS-BACKEND.md create mode 100644 projects/gamilit/orchestration/analisis-backend-2025-12-18/PLAN-IMPLEMENTACIONES.md create mode 100644 projects/gamilit/orchestration/analisis-frontend-validacion/FASE1-PLAN-ANALISIS.md create mode 100644 projects/gamilit/orchestration/analisis-frontend-validacion/FASE2-REPORTE-CONSOLIDADO.md create mode 100644 projects/gamilit/orchestration/analisis-frontend-validacion/FASE3-PLAN-IMPLEMENTACIONES.md create mode 100644 projects/gamilit/orchestration/analisis-frontend-validacion/FASE4-VALIDACION-DEPENDENCIAS.md create mode 100644 projects/gamilit/orchestration/analisis-frontend-validacion/FASE5-REPORTE-IMPLEMENTACIONES.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/EJECUTAR-AQUI.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/FASE4-VALIDACION-DEPENDENCIAS.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/FASE5-REPORTE-IMPLEMENTACION.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/HALLAZGOS-RESUMEN.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/INDEX.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/INSTRUCCIONES-EJECUCION.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/PLAN-ANALISIS-HOMOLOGACION.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/README.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/REPORTE-DDL-DIFERENCIAS.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/REPORTE-FINAL-CONSOLIDADO.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/REPORTE-SCRIPTS-DIFERENCIAS.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/REPORTE-SCRIPTS-PRINCIPALES.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/REPORTE-SEEDS-DIFERENCIAS.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/RESUMEN-EJECUTIVO.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/START-HERE.md create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/analyze_direct.py create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/compare-ddl.sh create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/compare_ddl.py create mode 100755 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/migrate-scripts.sh create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/quick-summary.sh create mode 100644 projects/gamilit/orchestration/analisis-homologacion-database-2025-12-18/run_comparison.sh create mode 100644 projects/gamilit/orchestration/analisis-migracion-2025-12-18/FASE1-ANALISIS-DIFERENCIAS.md create mode 100644 projects/gamilit/orchestration/analisis-migracion-2025-12-18/FASE2-ANALISIS-DETALLADO.md create mode 100644 projects/gamilit/orchestration/analisis-migracion-2025-12-18/FASE3-PLAN-IMPLEMENTACION.md create mode 100644 projects/gamilit/orchestration/analisis-migracion-2025-12-18/FASE4-VALIDACION-DEPENDENCIAS.md create mode 100644 projects/gamilit/orchestration/reportes/FASE1-ANALISIS-CAMBIOS-CODIGO-VS-DOCS.md create mode 100644 projects/gamilit/orchestration/reportes/FASE1-ANALISIS-DIFERENCIAS-DOCS.md create mode 100644 projects/gamilit/orchestration/reportes/FASE2-ANALISIS-COMPLETO-CONSOLIDADO.md create mode 100644 projects/gamilit/orchestration/reportes/FASE3-PLAN-IMPLEMENTACION-DOCUMENTACION.md create mode 100644 projects/gamilit/orchestration/reportes/FASE4-VALIDACION-PLAN-IMPLEMENTACION.md create mode 100644 projects/gamilit/orchestration/reportes/FASE5-REPORTE-FINAL-DOCUMENTACION.md create mode 100644 projects/gamilit/orchestration/reportes/REPORTE-ANALISIS-PRODUCCION-COMPLETO-2025-12-18.md create mode 100644 projects/gamilit/orchestration/reportes/REPORTE-HOMOLOGACION-DATABASE-2025-12-18.md create mode 100644 projects/gamilit/orchestration/reportes/REPORTE-HOMOLOGACION-DOCS-DESARROLLO-2025-12-18.md create mode 100755 projects/gamilit/scripts/setup-ssl-certbot.sh create mode 100755 projects/gamilit/scripts/validate-deployment.sh diff --git a/projects/gamilit/apps/backend/src/modules/auth/services/email-verification.service.ts b/projects/gamilit/apps/backend/src/modules/auth/services/email-verification.service.ts index afbe0ec..9915777 100644 --- a/projects/gamilit/apps/backend/src/modules/auth/services/email-verification.service.ts +++ b/projects/gamilit/apps/backend/src/modules/auth/services/email-verification.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException, BadRequestException, ConflictException } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException, ConflictException, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, IsNull } from 'typeorm'; import * as crypto from 'crypto'; @@ -6,6 +6,7 @@ import { User, EmailVerificationToken } from '../entities'; import { VerifyEmailDto, } from '../dto'; +import { MailService } from '@/modules/mail/mail.service'; /** * EmailVerificationService @@ -33,6 +34,8 @@ import { */ @Injectable() export class EmailVerificationService { + private readonly logger = new Logger(EmailVerificationService.name); + private readonly TOKEN_LENGTH_BYTES = 32; private readonly TOKEN_EXPIRATION_HOURS = 24; @@ -44,8 +47,7 @@ export class EmailVerificationService { @InjectRepository(EmailVerificationToken, 'auth') private readonly tokenRepository: Repository, - // TODO: Inject MailerService - // private readonly mailerService: MailerService, + private readonly mailService: MailService, ) {} /** @@ -89,9 +91,17 @@ export class EmailVerificationService { await this.tokenRepository.save(verificationToken); // 8. Enviar email con token plaintext - // TODO: Implementar envío de email - // await this.mailerService.sendEmailVerification(email, plainToken); - console.log(`[DEV] Email verification token for ${email}: ${plainToken}`); + try { + await this.mailService.sendVerificationEmail(email, plainToken); + this.logger.log(`Verification email sent to: ${email}`); + } catch (error) { + // Log error pero no fallar (el token ya está creado) + this.logger.error(`Failed to send verification email to ${email}:`, error); + // En desarrollo, mostrar el token para testing + if (process.env.NODE_ENV !== 'production') { + this.logger.debug(`[DEV] Verification token for ${email}: ${plainToken}`); + } + } return { message: 'Email de verificación enviado' }; } diff --git a/projects/gamilit/apps/backend/src/modules/auth/services/password-recovery.service.ts b/projects/gamilit/apps/backend/src/modules/auth/services/password-recovery.service.ts index 42f7260..b94920e 100644 --- a/projects/gamilit/apps/backend/src/modules/auth/services/password-recovery.service.ts +++ b/projects/gamilit/apps/backend/src/modules/auth/services/password-recovery.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException, BadRequestException } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository, IsNull } from 'typeorm'; import * as crypto from 'crypto'; @@ -9,6 +9,7 @@ import { ResetPasswordDto, } from '../dto'; import { MailService } from '@/modules/mail/mail.service'; +import { SessionManagementService } from './session-management.service'; /** * PasswordRecoveryService @@ -33,6 +34,8 @@ import { MailService } from '@/modules/mail/mail.service'; */ @Injectable() export class PasswordRecoveryService { + private readonly logger = new Logger(PasswordRecoveryService.name); + private readonly TOKEN_LENGTH_BYTES = 32; private readonly TOKEN_EXPIRATION_HOURS = 1; @@ -46,8 +49,7 @@ export class PasswordRecoveryService { private readonly mailService: MailService, - // TODO: Inject SessionManagementService for logout - // private readonly sessionService: SessionManagementService, + private readonly sessionManagementService: SessionManagementService, ) {} /** @@ -90,14 +92,16 @@ export class PasswordRecoveryService { // 7. Enviar email con token plaintext try { await this.mailService.sendPasswordResetEmail(user.email, plainToken); + this.logger.log(`Password reset email sent to: ${user.email}`); } catch (error) { // Log error pero NO fallar (por seguridad, no revelar errores) - console.error(`Failed to send password reset email to ${user.email}:`, error); + this.logger.error(`Failed to send password reset email to ${user.email}:`, error); + // En desarrollo, mostrar el token para testing + if (process.env.NODE_ENV !== 'production') { + this.logger.debug(`[DEV] Password reset token for ${user.email}: ${plainToken}`); + } } - // Fallback para desarrollo (si SMTP no configurado) - console.log(`[DEV] Password reset token for ${user.email}: ${plainToken}`); - return { message: genericMessage }; } @@ -159,10 +163,16 @@ export class PasswordRecoveryService { { used_at: new Date() }, ); - // 6. Invalidar todas las sesiones (logout global) - // TODO: Implementar con SessionManagementService - // await this.sessionService.revokeAllSessions(user.id); - console.log(`[DEV] Should revoke all sessions for user ${user.id}`); + // 6. Invalidar todas las sesiones (logout global de seguridad) + try { + // Revocar todas las sesiones excepto ninguna (currentSessionId vacío = revocar todas) + // Usamos un UUID inexistente para asegurar que se cierren TODAS las sesiones + const result = await this.sessionManagementService.revokeAllSessions(user.id, '00000000-0000-0000-0000-000000000000'); + this.logger.log(`Revoked ${result.count} sessions for user ${user.id} after password reset`); + } catch (error) { + // Log pero no fallar - la contraseña ya fue cambiada + this.logger.error(`Failed to revoke sessions for user ${user.id}:`, error); + } return { message: 'Contraseña actualizada exitosamente' }; } diff --git a/projects/gamilit/apps/backend/src/modules/progress/services/exercise-submission.service.ts b/projects/gamilit/apps/backend/src/modules/progress/services/exercise-submission.service.ts index 4a9ef25..31a1739 100644 --- a/projects/gamilit/apps/backend/src/modules/progress/services/exercise-submission.service.ts +++ b/projects/gamilit/apps/backend/src/modules/progress/services/exercise-submission.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException, BadRequestException, InternalServerErrorException } from '@nestjs/common'; +import { Injectable, NotFoundException, BadRequestException, InternalServerErrorException, Logger } from '@nestjs/common'; import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm'; import { Repository, EntityManager } from 'typeorm'; import { ExerciseSubmission } from '../entities'; @@ -79,6 +79,8 @@ interface FragmentState { */ @Injectable() export class ExerciseSubmissionService { + private readonly logger = new Logger(ExerciseSubmissionService.name); + constructor( @InjectRepository(ExerciseSubmission, 'progress') private readonly submissionRepo: Repository, @@ -242,7 +244,7 @@ export class ExerciseSubmissionService { ); } - console.log(`[BE-P2-009] Diario multimedia validation passed: ${wordCount} palabras`); + this.logger.log(`[BE-P2-009] Diario multimedia validation passed: ${wordCount} palabras`); } if (exercise.exercise_type === 'comic_digital') { @@ -269,7 +271,7 @@ export class ExerciseSubmissionService { ); } - console.log(`[BE-P2-009] Comic digital validation passed: ${panels.length} paneles`); + this.logger.log(`[BE-P2-009] Comic digital validation passed: ${panels.length} paneles`); } if (exercise.exercise_type === 'video_carta') { @@ -290,11 +292,11 @@ export class ExerciseSubmissionService { ); } - console.log(`[BE-P2-009] Video carta validation passed: ${videoUrl}`); + this.logger.log(`[BE-P2-009] Video carta validation passed: ${videoUrl}`); } // FE-059: Validate answer structure BEFORE saving to database - console.log(`[FE-059] Validating answer structure for exercise type: ${exercise.exercise_type}`); + this.logger.log(`[FE-059] Validating answer structure for exercise type: ${exercise.exercise_type}`); await ExerciseAnswerValidator.validate(exercise.exercise_type, answers); // Verificar si ya existe un envío previo @@ -326,7 +328,7 @@ export class ExerciseSubmissionService { // ✅ FIX BUG-001: Auto-claim rewards después de calificar if (submission.is_correct && submission.status === 'graded') { - console.log(`[BUG-001 FIX] Auto-claiming rewards for submission ${submission.id}`); + this.logger.log(`[BUG-001 FIX] Auto-claiming rewards for submission ${submission.id}`); const rewards = await this.claimRewards(submission.id); // Los campos ya están persistidos en la submission por claimRewards() @@ -336,13 +338,13 @@ export class ExerciseSubmissionService { // 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`); + this.logger.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}`); + this.logger.error(`[BE-P2-008] Failed to notify teacher: ${errorMessage}`); } } @@ -371,7 +373,7 @@ export class ExerciseSubmissionService { // P1-003: Check if manual grading is requested if (manualGrade?.final_score !== undefined) { - console.log(`[P1-003] Manual grading requested: score=${manualGrade.final_score}, grader=${manualGrade.grader_id}`); + this.logger.log(`[P1-003] Manual grading requested: score=${manualGrade.final_score}, grader=${manualGrade.grader_id}`); // Validate manual score range if (manualGrade.final_score < 0 || manualGrade.final_score > submission.max_score) { @@ -398,7 +400,7 @@ export class ExerciseSubmissionService { submission.feedback = `Calificación manual: ${manualGrade.final_score}/${submission.max_score}`; } - console.log(`[P1-003] Manual grading applied: ${submission.score}/${submission.max_score}, correct=${submission.is_correct}`); + this.logger.log(`[P1-003] Manual grading applied: ${submission.score}/${submission.max_score}, correct=${submission.is_correct}`); const savedSubmission = await this.submissionRepo.save(submission); @@ -406,17 +408,17 @@ export class ExerciseSubmissionService { try { const earned = await this.achievementsService.detectAndGrantEarned(submission.user_id); if (earned.length > 0) { - console.log(`[IMPL-004] ✅ Granted ${earned.length} achievements to user ${submission.user_id} after manual grading`); + this.logger.log(`[IMPL-004] ✅ Granted ${earned.length} achievements to user ${submission.user_id} after manual grading`); } } catch (achievementError) { - console.error(`[IMPL-004] ❌ Error detecting achievements: ${achievementError instanceof Error ? achievementError.message : String(achievementError)}`); + this.logger.error(`[IMPL-004] ❌ Error detecting achievements: ${achievementError instanceof Error ? achievementError.message : String(achievementError)}`); } return savedSubmission; } // Default: Auto-grading using SQL validate_and_audit() - console.log('[P1-003] No manual score provided - executing auto-grading'); + this.logger.log('[P1-003] No manual score provided - executing auto-grading'); const { score, isCorrect, correctAnswers, totalQuestions, feedback, details, auditId } = await this.autoGrade( submission.user_id, // userId (profiles.id) @@ -433,7 +435,7 @@ export class ExerciseSubmissionService { // FE-059: Audit ID is stored in educational_content.exercise_validation_audit // Can be queried using: exercise_id + user_id + attempt_number - console.log(`[FE-059] Validation audit saved with ID: ${auditId}`); + this.logger.log(`[FE-059] Validation audit saved with ID: ${auditId}`); // Store validation results in submission (submission as any).correctAnswers = correctAnswers; @@ -466,10 +468,10 @@ export class ExerciseSubmissionService { try { const earned = await this.achievementsService.detectAndGrantEarned(submission.user_id); if (earned.length > 0) { - console.log(`[IMPL-004] ✅ Granted ${earned.length} achievements to user ${submission.user_id} after auto-grading`); + this.logger.log(`[IMPL-004] ✅ Granted ${earned.length} achievements to user ${submission.user_id} after auto-grading`); } } catch (achievementError) { - console.error(`[IMPL-004] ❌ Error detecting achievements: ${achievementError instanceof Error ? achievementError.message : String(achievementError)}`); + this.logger.error(`[IMPL-004] ❌ Error detecting achievements: ${achievementError instanceof Error ? achievementError.message : String(achievementError)}`); } return savedSubmission; @@ -512,7 +514,7 @@ export class ExerciseSubmissionService { // SPECIAL CASE: Completar Espacios - Anti-redundancy validation (Exercise 1.3) if (exercise.exercise_type === 'completar_espacios') { - console.log('[autoGrade] Checking anti-redundancy for Completar Espacios (Exercise 1.3)'); + this.logger.log('[autoGrade] Checking anti-redundancy for Completar Espacios (Exercise 1.3)'); // Check if blanks.5 and blanks.6 exist and are identical (case-insensitive) const blanks = (answerData.blanks || {}) as Record; @@ -521,7 +523,7 @@ export class ExerciseSubmissionService { const space6 = String(blanks['6']).toLowerCase().trim(); if (space5 === space6) { - console.log(`[autoGrade] REDUNDANCY DETECTED: space5="${space5}" === space6="${space6}"`); + this.logger.log(`[autoGrade] REDUNDANCY DETECTED: space5="${space5}" === space6="${space6}"`); // Create audit record for failed validation const auditId = 'redundancy-' + Date.now(); @@ -545,12 +547,12 @@ export class ExerciseSubmissionService { } } - console.log('[autoGrade] Anti-redundancy check passed, proceeding with normal validation'); + this.logger.log('[autoGrade] Anti-redundancy check passed, proceeding with normal validation'); } // SPECIAL CASE: Rueda de Inferencias custom validation if (exercise.exercise_type === 'rueda_inferencias') { - console.log('[autoGrade] Using custom validation for Rueda de Inferencias'); + this.logger.log('[autoGrade] Using custom validation for Rueda de Inferencias'); // Extract fragmentStates from answerData if available const fragmentStates = answerData.fragmentStates as FragmentState[] | undefined; @@ -583,7 +585,7 @@ export class ExerciseSubmissionService { } // DEFAULT CASE: Use SQL validate_and_audit() for other exercise types - console.log(`[FE-059] Validating exercise ${exerciseId} using SQL validate_and_audit()`); + this.logger.log(`[FE-059] Validating exercise ${exerciseId} using SQL validate_and_audit()`); // Call PostgreSQL validate_and_audit() function const query = ` @@ -611,7 +613,7 @@ export class ExerciseSubmissionService { const validation = result[0]; - console.log(`[FE-059] Validation result: score=${validation.score}/${validation.max_score}, correct=${validation.is_correct}, audit_id=${validation.audit_id}`); + this.logger.log(`[FE-059] Validation result: score=${validation.score}/${validation.max_score}, correct=${validation.is_correct}, audit_id=${validation.audit_id}`); return { score: validation.score, @@ -623,7 +625,7 @@ export class ExerciseSubmissionService { auditId: validation.audit_id, }; } catch (error) { - console.error('[FE-059] Error calling validate_and_audit():', error); + this.logger.error('[FE-059] Error calling validate_and_audit():', error); const errorMessage = error instanceof Error ? error.message : 'Unknown error'; throw new InternalServerErrorException(`Exercise validation failed: ${errorMessage}`); } @@ -780,7 +782,7 @@ export class ExerciseSubmissionService { }>; }; } { - console.log('[validateRuedaInferencias] Starting validation for Rueda de Inferencias exercise'); + this.logger.log('[validateRuedaInferencias] Starting validation for Rueda de Inferencias exercise'); // Cast solution to ExerciseSolution interface const solution = exercise.solution as unknown as ExerciseSolution; @@ -816,7 +818,7 @@ export class ExerciseSubmissionService { // Skip if no answer provided if (!userAnswer) { - console.log(`[validateRuedaInferencias] No answer provided for fragment ${fragment.id}, skipping`); + this.logger.log(`[validateRuedaInferencias] No answer provided for fragment ${fragment.id}, skipping`); continue; } @@ -830,14 +832,14 @@ export class ExerciseSubmissionService { } } - console.log(`[validateRuedaInferencias] Fragment ${fragment.id} using category: ${categoryId}`); + this.logger.log(`[validateRuedaInferencias] Fragment ${fragment.id} using category: ${categoryId}`); // Get expectations for this category (with type safety) type CategoryId = 'cat-literal' | 'cat-inferencial' | 'cat-critico' | 'cat-creativo'; let categoryExpectation = fragment.categoryExpectations?.[categoryId as CategoryId]; if (!categoryExpectation) { - console.warn(`[validateRuedaInferencias] No expectations found for category ${categoryId} in fragment ${fragment.id}, using default`); + this.logger.warn(`[validateRuedaInferencias] No expectations found for category ${categoryId} in fragment ${fragment.id}, using default`); // Fallback: use literal category if available categoryExpectation = fragment.categoryExpectations?.['cat-literal']; if (!categoryExpectation) { @@ -847,7 +849,7 @@ export class ExerciseSubmissionService { // Validate categoryExpectation structure if (!categoryExpectation.keywords || !Array.isArray(categoryExpectation.keywords)) { - console.warn(`[validateRuedaInferencias] Invalid category expectation for ${categoryId} in fragment ${fragment.id}`); + this.logger.warn(`[validateRuedaInferencias] Invalid category expectation for ${categoryId} in fragment ${fragment.id}`); continue; } @@ -861,7 +863,7 @@ export class ExerciseSubmissionService { userAnswerLower.includes(keyword.toLowerCase()), ); - console.log(`[validateRuedaInferencias] Fragment ${fragment.id}: Found ${foundKeywords.length}/${expectedKeywords.length} keywords`); + this.logger.log(`[validateRuedaInferencias] Fragment ${fragment.id}: Found ${foundKeywords.length}/${expectedKeywords.length} keywords`); // Calculate score based on keywords found let fragmentScore = 0; @@ -910,7 +912,7 @@ export class ExerciseSubmissionService { overallFeedback = 'Necesitas practicar más. Revisa las categorías de inferencias y los ejemplos proporcionados.'; } - console.log(`[validateRuedaInferencias] Validation complete: ${totalScore}/${maxScore} points (${overallPercentage.toFixed(1)}%)`); + this.logger.log(`[validateRuedaInferencias] Validation complete: ${totalScore}/${maxScore} points (${overallPercentage.toFixed(1)}%)`); return { score: totalScore, @@ -973,7 +975,7 @@ export class ExerciseSubmissionService { let xpEarned = Math.floor(baseXpReward * scoreMultiplier * rankMultiplier); let mlCoinsEarned = Math.floor(baseMlCoinsReward * scoreMultiplier); - console.log(`[claimRewards] XP calculation: base=${baseXpReward}, score=${scoreMultiplier.toFixed(2)}, rank=${rankMultiplier}x, total=${xpEarned}`); + this.logger.log(`[claimRewards] XP calculation: base=${baseXpReward}, score=${scoreMultiplier.toFixed(2)}, rank=${rankMultiplier}x, total=${xpEarned}`); // Bonificación por perfect score if (submission.score === submission.max_score && !submission.hint_used) { @@ -989,7 +991,7 @@ export class ExerciseSubmissionService { mlCoinsEarned = Math.max(0, mlCoinsEarned - submission.ml_coins_spent); // ✅ FIX BUG-001: Actualizar user_stats con XP y ML Coins - console.log(`[BUG-001 FIX] Claiming rewards for user ${submission.user_id}: +${xpEarned} XP, +${mlCoinsEarned} ML Coins`); + this.logger.log(`[BUG-001 FIX] Claiming rewards for user ${submission.user_id}: +${xpEarned} XP, +${mlCoinsEarned} ML Coins`); // Obtener rango ANTES de agregar XP const userStatsBefore = await this.userStatsService.findByUserId(submission.user_id); @@ -1048,7 +1050,7 @@ export class ExerciseSubmissionService { newMultiplier: rankMultipliers[newRank] || 1.0, }; - console.log(`[RANK UP] User ${submission.user_id} promoted from ${previousRank} to ${newRank}`); + this.logger.log(`[RANK UP] User ${submission.user_id} promoted from ${previousRank} to ${newRank}`); } // ✅ FIX BUG-002: Actualizar module_progress después de completar ejercicio @@ -1107,7 +1109,7 @@ export class ExerciseSubmissionService { return 1.00; // Default si no encuentra } catch { - console.warn(`[getRankXpMultiplier] Error getting multiplier for user ${userId}, using 1.00`); + this.logger.warn(`[getRankXpMultiplier] Error getting multiplier for user ${userId}, using 1.00`); return 1.00; } } @@ -1133,12 +1135,12 @@ export class ExerciseSubmissionService { // Obtener module_id del ejercicio const exercise = await this.exerciseRepo.findOne({ where: { id: exerciseId } }); if (!exercise?.module_id) { - console.warn(`[BUG-002 FIX] Exercise ${exerciseId} has no module_id - skipping progress update`); + this.logger.warn(`[BUG-002 FIX] Exercise ${exerciseId} has no module_id - skipping progress update`); return; } const moduleId = exercise.module_id; - console.log(`[BUG-002 FIX] Updating module progress for user ${userId}, module ${moduleId}`); + this.logger.log(`[BUG-002 FIX] Updating module progress for user ${userId}, module ${moduleId}`); // Verificar si es el primer envío correcto para ESTE ejercicio específico const previousCorrectSubmissions = await this.submissionRepo.count({ @@ -1156,7 +1158,7 @@ export class ExerciseSubmissionService { SET last_accessed_at = NOW(), updated_at = NOW() WHERE user_id = $1 AND module_id = $2 `, [userId, moduleId]); - console.log('[BUG-002 FIX] Not first correct submission - only updated timestamps'); + this.logger.log('[BUG-002 FIX] Not first correct submission - only updated timestamps'); return; } @@ -1192,7 +1194,7 @@ export class ExerciseSubmissionService { newStatus = 'not_started'; } - console.log(`[BUG-002 FIX] Module progress: ${completedExercises}/${totalExercises} (${progressPercentage}%) - Status: ${newStatus}`); + this.logger.log(`[BUG-002 FIX] Module progress: ${completedExercises}/${totalExercises} (${progressPercentage}%) - Status: ${newStatus}`); // Actualizar o insertar module_progress usando UPSERT await this.entityManager.query(` @@ -1228,12 +1230,12 @@ export class ExerciseSubmissionService { updated_at = NOW() `, [userId, moduleId, newStatus, progressPercentage, completedExercises, totalExercises, xpEarned, mlCoinsEarned]); - console.log('[BUG-002 FIX] ✅ Module progress updated successfully'); + this.logger.log('[BUG-002 FIX] ✅ Module progress updated successfully'); } catch (error) { // Log error pero no bloquear el claim de rewards const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[BUG-002 FIX] ❌ Error updating module progress: ${errorMessage}`); + this.logger.error(`[BUG-002 FIX] ❌ Error updating module progress: ${errorMessage}`); // No throw - la actualización de progreso no debe bloquear la respuesta } } @@ -1252,7 +1254,7 @@ export class ExerciseSubmissionService { */ private async updateMissionsProgressAfterCompletion(userId: string, xpEarned: number = 0): Promise { try { - console.log(`[BUG-003 FIX] Updating missions progress for user ${userId}`); + this.logger.log(`[BUG-003 FIX] Updating missions progress for user ${userId}`); // Buscar misiones activas del usuario con objetivo 'complete_exercises' const missions = await this.missionsService.findByTypeAndUser(userId, MissionTypeEnum.DAILY); @@ -1267,7 +1269,7 @@ export class ExerciseSubmissionService { ); if (activeMissions.length === 0) { - console.log('[BUG-003 FIX] No active missions with \'complete_exercises\' objective found'); + this.logger.log('[BUG-003 FIX] No active missions with \'complete_exercises\' objective found'); return; } @@ -1280,11 +1282,11 @@ export class ExerciseSubmissionService { 'complete_exercises', 1, // Incrementar en 1 por cada ejercicio completado ); - console.log(`[BUG-003 FIX] ✅ Mission ${mission.id} (complete_exercises) updated`); + this.logger.log(`[BUG-003 FIX] ✅ Mission ${mission.id} (complete_exercises) updated`); } catch (missionError) { // Log pero continuar con otras misiones const errorMessage = missionError instanceof Error ? missionError.message : String(missionError); - console.warn(`[BUG-003 FIX] ⚠️ Error updating mission ${mission.id}: ${errorMessage}`); + this.logger.warn(`[BUG-003 FIX] ⚠️ Error updating mission ${mission.id}: ${errorMessage}`); } } @@ -1304,20 +1306,20 @@ export class ExerciseSubmissionService { 'earn_xp', xpEarned, // Incrementar por cantidad de XP ganado ); - console.log(`[BUG-003 FIX] ✅ Mission ${mission.id} (earn_xp) updated with +${xpEarned} XP`); + this.logger.log(`[BUG-003 FIX] ✅ Mission ${mission.id} (earn_xp) updated with +${xpEarned} XP`); } catch (missionError) { const errorMessage = missionError instanceof Error ? missionError.message : String(missionError); - console.warn(`[BUG-003 FIX] ⚠️ Error updating earn_xp mission ${mission.id}: ${errorMessage}`); + this.logger.warn(`[BUG-003 FIX] ⚠️ Error updating earn_xp mission ${mission.id}: ${errorMessage}`); } } } - console.log('[BUG-003 FIX] ✅ Missions progress update completed'); + this.logger.log('[BUG-003 FIX] ✅ Missions progress update completed'); } catch (error) { // Log error pero no bloquear el claim de rewards const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[BUG-003 FIX] ❌ Error updating missions progress: ${errorMessage}`); + this.logger.error(`[BUG-003 FIX] ❌ Error updating missions progress: ${errorMessage}`); // No throw - la actualización de misiones no debe bloquear la respuesta } } @@ -1486,7 +1488,7 @@ export class ExerciseSubmissionService { exercise: Exercise, studentProfileId: string, ): Promise { - console.log(`[BE-P2-008] Notifying teacher about submission ${submission.id}`); + this.logger.log(`[BE-P2-008] Notifying teacher about submission ${submission.id}`); // 1. Obtener datos del estudiante const studentProfile = await this.profileRepo.findOne({ @@ -1495,7 +1497,7 @@ export class ExerciseSubmissionService { }); if (!studentProfile) { - console.warn(`[BE-P2-008] Student profile ${studentProfileId} not found - skipping notification`); + this.logger.warn(`[BE-P2-008] Student profile ${studentProfileId} not found - skipping notification`); return; } @@ -1524,7 +1526,7 @@ export class ExerciseSubmissionService { const teacherResult = await this.entityManager.query(teacherQuery, [studentProfileId]); if (!teacherResult || teacherResult.length === 0) { - console.warn(`[BE-P2-008] No active teacher found for student ${studentProfileId} - skipping notification`); + this.logger.warn(`[BE-P2-008] No active teacher found for student ${studentProfileId} - skipping notification`); return; } @@ -1535,7 +1537,7 @@ export class ExerciseSubmissionService { const classroomName = teacher.classroom_name || 'tu aula'; const teacherPreferences = teacher.teacher_preferences || {}; - console.log(`[BE-P2-008] Found teacher ${teacherId} (${teacherEmail}) for student ${studentProfileId}`); + this.logger.log(`[BE-P2-008] Found teacher ${teacherId} (${teacherEmail}) for student ${studentProfileId}`); // 3. Construir URL de revisión const reviewUrl = `/teacher/reviews/${submission.id}`; @@ -1565,10 +1567,10 @@ export class ExerciseSubmissionService { priority: 'high', }); - console.log(`[BE-P2-008] ✅ In-app notification sent to teacher ${teacherId}`); + this.logger.log(`[BE-P2-008] ✅ In-app notification sent to teacher ${teacherId}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[BE-P2-008] ❌ Failed to send in-app notification: ${errorMessage}`); + this.logger.error(`[BE-P2-008] ❌ Failed to send in-app notification: ${errorMessage}`); } // 5. Enviar email si está habilitado en preferencias @@ -1576,7 +1578,7 @@ export class ExerciseSubmissionService { const exerciseFeedbackEmailEnabled = teacherPreferences?.email_notifications?.exercise_feedback !== false; if (emailNotificationsEnabled && exerciseFeedbackEmailEnabled && teacherEmail) { - console.log(`[BE-P2-008] Email notifications enabled for teacher ${teacherId} - sending email to ${teacherEmail}`); + this.logger.log(`[BE-P2-008] Email notifications enabled for teacher ${teacherId} - sending email to ${teacherEmail}`); const emailSubject = `Nuevo ejercicio para revisar: ${exercise.title}`; const emailMessage = ` @@ -1597,19 +1599,19 @@ export class ExerciseSubmissionService { ); if (emailSent) { - console.log(`[BE-P2-008] ✅ Email notification sent to teacher ${teacherEmail}`); + this.logger.log(`[BE-P2-008] ✅ Email notification sent to teacher ${teacherEmail}`); } else { - console.warn(`[BE-P2-008] ⚠️ Email notification logged but not sent (SMTP not configured)`); + this.logger.warn(`[BE-P2-008] ⚠️ Email notification logged but not sent (SMTP not configured)`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[BE-P2-008] ❌ Failed to send email notification: ${errorMessage}`); + this.logger.error(`[BE-P2-008] ❌ Failed to send email notification: ${errorMessage}`); } } else { - console.log(`[BE-P2-008] Email notifications disabled for teacher ${teacherId} - skipping email`); + this.logger.log(`[BE-P2-008] Email notifications disabled for teacher ${teacherId} - skipping email`); } - console.log(`[BE-P2-008] ✅ Teacher notification process completed for submission ${submission.id}`); + this.logger.log(`[BE-P2-008] ✅ Teacher notification process completed for submission ${submission.id}`); } /** diff --git a/projects/gamilit/apps/backend/src/modules/teacher/services/student-progress.service.ts b/projects/gamilit/apps/backend/src/modules/teacher/services/student-progress.service.ts index 199070e..f0e3f3b 100644 --- a/projects/gamilit/apps/backend/src/modules/teacher/services/student-progress.service.ts +++ b/projects/gamilit/apps/backend/src/modules/teacher/services/student-progress.service.ts @@ -296,7 +296,7 @@ export class StudentProgressService { module_order: moduleData?.order_index || 1, total_activities: totalActivities, completed_activities: Math.round((mp.progress_percentage / 100) * totalActivities), - average_score: Math.round(mp.score || mp.progress_percentage * 0.8), + average_score: Math.round(mp.average_score || mp.progress_percentage * 0.8), time_spent_minutes: Math.round(timeSpentSeconds / 60), last_activity_date: mp.updated_at, status: this.calculateModuleStatus(mp.progress_percentage), diff --git a/projects/gamilit/apps/database/scripts/INDEX.md b/projects/gamilit/apps/database/scripts/INDEX.md new file mode 100644 index 0000000..92412ff --- /dev/null +++ b/projects/gamilit/apps/database/scripts/INDEX.md @@ -0,0 +1,396 @@ +# 📚 ÍNDICE MAESTRO - Scripts de Base de Datos GAMILIT + +**Actualizado:** 2025-11-08 +**Versión:** 3.0 +**Estado:** ✅ Consolidado y Funcional + +--- + +## 🎯 INICIO RÁPIDO + +¿Nuevo en el proyecto? Empieza aquí: + +```bash +# 1. Lee la guía rápida +cat QUICK-START.md + +# 2. Inicializa la BD +./init-database.sh --env dev --force + +# 3. ¡Listo! +``` + +--- + +## 📖 DOCUMENTACIÓN DISPONIBLE + +### Documentos Principales + +| Archivo | Propósito | ¿Cuándo leer? | +|---------|-----------|---------------| +| **QUICK-START.md** | Guía rápida de uso | ⭐ Primero - Setup inicial | +| **README.md** | Documentación completa | Segunda lectura - Detalles | +| **ANALISIS-SCRIPTS-2025-11-08.md** | Análisis técnico | Referencia técnica | +| **INDEX.md** | Este índice | Navegación general | +| **README-SETUP.md** | Guía de setup detallada | Setup avanzado | + +### Orden Recomendado de Lectura + +``` +1. INDEX.md (este archivo) ← Estás aquí +2. QUICK-START.md ← Guía rápida para empezar +3. README.md ← Documentación completa +4. ANALISIS-SCRIPTS-2025-11-08.md ← Detalles técnicos (opcional) +``` + +--- + +## 🛠️ SCRIPTS DISPONIBLES + +### Scripts Principales (3) ⭐ + +| Script | Tamaño | Estado | Propósito | +|--------|--------|--------|-----------| +| `init-database.sh` | 36K | ✅ Activo | Inicialización completa (v3.0) | +| `reset-database.sh` | 16K | ✅ Activo | Reset rápido (mantiene usuario) | +| `recreate-database.sh` | 8.9K | ✅ Activo | Recreación completa (elimina todo) | + +### Scripts de Gestión (3) + +| Script | Tamaño | Estado | Propósito | +|--------|--------|--------|-----------| +| `manage-secrets.sh` | 18K | ✅ Activo | Gestión de secrets con dotenv-vault | +| `update-env-files.sh` | 16K | ✅ Activo | Sincronización de .env | +| `cleanup-duplicados.sh` | 12K | ✅ Activo | Limpieza de duplicados | + +### Scripts de Inventario (8) + +Ubicación: `inventory/` + +| Script | Propósito | +|--------|-----------| +| `list-tables.sh` | Listar todas las tablas | +| `list-functions.sh` | Listar todas las funciones | +| `list-enums.sh` | Listar todos los ENUMs | +| `list-rls.sh` | Listar RLS policies | +| `list-indexes.sh` | Listar índices | +| `list-views.sh` | Listar vistas | +| `list-triggers.sh` | Listar triggers | +| `list-seeds.sh` | Listar seeds disponibles | +| `generate-all-inventories.sh` | Generar todos los inventarios | + +### Scripts Obsoletos (deprecated/) + +| Script | Estado | Notas | +|--------|--------|-------| +| `init-database-v1.sh` | 📦 Deprecated | Versión original (21K) | +| `init-database-v2.sh` | 📦 Deprecated | Versión intermedia (32K) | +| `init-database.sh.backup-*` | 📦 Deprecated | Backup de v1.0 | + +⚠️ **NO eliminar archivos en deprecated/** - Son históricos y de referencia + +--- + +## 📊 COMPARACIÓN RÁPIDA DE SCRIPTS PRINCIPALES + +| Característica | init-database.sh | reset-database.sh | recreate-database.sh | +|----------------|------------------|-------------------|----------------------| +| **Elimina usuario** | ❌ | ❌ | ✅ | +| **Elimina BD** | ⚠️ Si existe | ✅ | ✅ | +| **Crea usuario** | ✅ Si no existe | ❌ | ✅ | +| **Genera password** | ✅ | ❌ | ✅ | +| **Requiere password** | ❌ | ✅ | ❌ | +| **Actualiza .env** | ✅ | ❌ | ✅ | +| **Soporta dotenv-vault** | ✅ | ❌ | ✅ (vía init) | +| **Tiempo ejecución** | 30-60s | 20-30s | 40-70s | +| **Riesgo de pérdida datos** | Bajo | Medio | Alto | + +--- + +## 🎯 GUÍA DE DECISIÓN RÁPIDA + +### ¿Qué script debo usar? + +``` +┌─────────────────────────────────────┐ +│ ¿Es la primera vez en el proyecto? │ +└──────────┬──────────────────────────┘ + │ + ├─ SÍ ──> init-database.sh --env dev --force + │ + └─ NO ──┐ + │ + ┌──────────┴────────────────────────┐ + │ ¿Conoces el password del usuario? │ + └──────────┬────────────────────────┘ + │ + ├─ SÍ ──> reset-database.sh --env dev --password "pass" + │ + └─ NO ──> recreate-database.sh --env dev +``` + +### Casos de Uso Específicos + +| Situación | Script Recomendado | Comando | +|-----------|-------------------|---------| +| **Primera vez** | init-database.sh | `./init-database.sh --env dev --force` | +| **Aplicar cambios DDL** | reset-database.sh | `./reset-database.sh --env dev --password "pass"` | +| **Olvidé password** | recreate-database.sh | `./recreate-database.sh --env dev` | +| **Deployment producción** | init-database.sh + vault | `./manage-secrets.sh generate --env prod && ./init-database.sh --env prod` | +| **Desarrollo diario** | reset-database.sh | `./reset-database.sh --env dev --password "$(grep Password ../database-credentials-dev.txt | cut -d: -f2 | xargs)"` | + +--- + +## 📁 ESTRUCTURA DEL DIRECTORIO + +``` +/apps/database/scripts/ +│ +├── 📖 Documentación +│ ├── INDEX.md ← Estás aquí +│ ├── QUICK-START.md ⭐ Guía rápida +│ ├── README.md 📚 Documentación completa +│ ├── README-SETUP.md 🔧 Setup avanzado +│ └── ANALISIS-SCRIPTS-2025-11-08.md 📊 Análisis técnico +│ +├── 🛠️ Scripts Principales +│ ├── init-database.sh ⭐ Inicialización (v3.0) +│ ├── reset-database.sh 🔄 Reset rápido +│ └── recreate-database.sh ⚠️ Recreación completa +│ +├── 🔐 Scripts de Gestión +│ ├── manage-secrets.sh 🔑 Gestión de secrets +│ ├── update-env-files.sh 🔧 Sincronización .env +│ └── cleanup-duplicados.sh 🧹 Limpieza +│ +├── ⚙️ Configuración +│ └── config/ +│ ├── dev.conf 🛠️ Config desarrollo +│ └── prod.conf 🚀 Config producción +│ +├── 📊 Inventario +│ └── inventory/ +│ ├── list-tables.sh 📋 Listar tablas +│ ├── list-functions.sh ⚙️ Listar funciones +│ ├── list-enums.sh 🏷️ Listar ENUMs +│ ├── list-rls.sh 🔒 Listar RLS +│ ├── list-indexes.sh 📈 Listar índices +│ ├── list-views.sh 👁️ Listar vistas +│ ├── list-triggers.sh ⚡ Listar triggers +│ ├── list-seeds.sh 🌱 Listar seeds +│ └── generate-all-inventories.sh 📊 Generar todos +│ +├── 🔄 Migraciones +│ └── migrations/ +│ └── *.sql 📝 Migraciones SQL +│ +├── 💾 Backup y Restore +│ ├── backup/ 💾 Scripts de backup +│ └── restore/ ♻️ Scripts de restore +│ +├── 🛠️ Utilidades +│ └── utilities/ 🔧 Herramientas varias +│ +└── 📦 Obsoletos + └── deprecated/ + ├── init-database-v1.sh 📦 Versión 1.0 + ├── init-database-v2.sh 📦 Versión 2.0 + └── init-database.sh.backup-* 📦 Backups +``` + +--- + +## 🔍 BÚSQUEDA RÁPIDA + +### ¿Cómo hacer...? + +**Inicializar BD por primera vez:** +```bash +./init-database.sh --env dev --force +``` + +**Resetear datos rápidamente:** +```bash +PASSWORD=$(grep 'Database Password' ../database-credentials-dev.txt | cut -d: -f2 | xargs) +./reset-database.sh --env dev --password "$PASSWORD" +``` + +**Ver credenciales actuales:** +```bash +cat ../database-credentials-dev.txt +``` + +**Listar todos los objetos de BD:** +```bash +cd inventory/ +./generate-all-inventories.sh +``` + +**Aplicar migración SQL:** +```bash +# Agregar migración a migrations/ +# Luego resetear BD +./reset-database.sh --env dev --password "pass" +``` + +**Verificar estado de BD:** +```bash +# Verificar conexión +psql -U gamilit_user -d gamilit_platform -c "SELECT version();" + +# Contar objetos +psql -U gamilit_user -d gamilit_platform -c "\dt *.*" | wc -l # Tablas +psql -U gamilit_user -d gamilit_platform -c "\df *.*" | wc -l # Funciones +psql -U gamilit_user -d gamilit_platform -c "\dn" | wc -l # Schemas +``` + +--- + +## 📊 ESTADO DE LA BASE DE DATOS + +### Objetos Implementados (según INVENTARIO-COMPLETO-BD-2025-11-07.md) + +| Tipo de Objeto | Cantidad | Estado | +|----------------|----------|--------| +| **Schemas** | 13 | ✅ Completo | +| **Tablas** | 61 | ✅ Completo | +| **Funciones** | 61 | ✅ Completo | +| **Vistas** | 12 | ✅ Completo | +| **Vistas Materializadas** | 4 | ✅ Completo | +| **Triggers** | 49 | ✅ Completo | +| **Índices** | 74 archivos | ✅ Completo | +| **RLS Policies** | 24 archivos | ✅ Completo | +| **ENUMs** | 36 | ✅ Completo | + +**Total:** 285 archivos SQL + +**Calidad:** A+ (98.8%) + +--- + +## ⚠️ ADVERTENCIAS IMPORTANTES + +### Desarrollo (dev) + +✅ **Puedes:** +- Usar `--force` libremente +- Recrear BD frecuentemente +- Experimentar con scripts + +❌ **Evita:** +- Usar secrets de producción +- Omitir logs de errores + +### Producción (prod) + +✅ **Debes:** +- SIEMPRE hacer backup primero +- Usar dotenv-vault +- Validar dos veces +- Notificar al equipo + +❌ **NUNCA:** +- Usar `--force` sin validación +- Recrear sin backup +- Ejecutar sin pruebas previas + +--- + +## 🐛 TROUBLESHOOTING RÁPIDO + +| Error | Solución Rápida | +|-------|----------------| +| "psql no encontrado" | `sudo apt install postgresql-client` | +| "No se puede conectar" | `sudo systemctl start postgresql` | +| "Usuario ya existe" | `./recreate-database.sh --env dev` | +| "Permisos denegados" | `chmod +x *.sh` | +| "BD en uso" | `sudo -u postgres psql -c "SELECT pg_terminate_backend..."` | + +Para más detalles: `cat QUICK-START.md | grep -A 10 "Troubleshooting"` + +--- + +## 📞 OBTENER AYUDA + +### Orden de consulta + +1. **QUICK-START.md** - Casos de uso comunes +2. **README.md** - Documentación detallada +3. **ANALISIS-SCRIPTS-2025-11-08.md** - Detalles técnicos +4. **Logs del script** - Revisa el output del comando +5. **Equipo de BD** - Si todo falla + +### Comandos de ayuda + +```bash +# Ver ayuda de cualquier script +./init-database.sh --help +./reset-database.sh --help +./recreate-database.sh --help +``` + +--- + +## ✅ CHECKLIST RÁPIDO + +### Primera Vez en el Proyecto + +- [ ] Leí QUICK-START.md +- [ ] PostgreSQL está instalado y corriendo +- [ ] Ejecuté `./init-database.sh --env dev --force` +- [ ] Verifiqué credenciales en `../database-credentials-dev.txt` +- [ ] Backend puede conectarse a la BD + +### Antes de Deployment Producción + +- [ ] Leí README.md completo +- [ ] Tengo backup completo de BD actual +- [ ] Generé secrets con `manage-secrets.sh` +- [ ] Probé en staging +- [ ] Tengo plan de rollback +- [ ] Notifiqué al equipo + +--- + +## 📈 HISTORIAL DE CAMBIOS + +### 2025-11-08 - Consolidación v3.0 + +- ✅ Unificadas versiones múltiples de init-database.sh +- ✅ Movidos scripts obsoletos a deprecated/ +- ✅ Creado QUICK-START.md +- ✅ Creado ANALISIS-SCRIPTS-2025-11-08.md +- ✅ Creado INDEX.md (este archivo) +- ✅ Actualizada documentación completa + +### Versiones Anteriores + +- v2.0 (2025-11-02) - Integración con update-env-files +- v1.0 (Original) - Scripts base + +--- + +## 🎓 RECURSOS ADICIONALES + +### Documentación de BD + +- `INVENTARIO-COMPLETO-BD-2025-11-07.md` - Inventario exhaustivo +- `REPORTE-VALIDACION-BD-COMPLETO-2025-11-08.md` - Validación completa +- `MATRIZ-COBERTURA-MODULOS-PLATAFORMA-2025-11-07.md` - Cobertura + +### Validaciones Cruzadas + +- `VALIDACION-CRUZADA-INFORME-MIGRACION-2025-11-08.md` - Validación de migración + +--- + +**Última actualización:** 2025-11-08 +**Mantenido por:** Equipo de Base de Datos GAMILIT +**Versión:** 3.0 +**Estado:** ✅ Consolidado y Funcional + +--- + +🎉 **¡Bienvenido a los Scripts de Base de Datos GAMILIT!** 🎉 + +**Próximo paso:** Lee `QUICK-START.md` para empezar diff --git a/projects/gamilit/apps/database/scripts/QUICK-START.md b/projects/gamilit/apps/database/scripts/QUICK-START.md new file mode 100644 index 0000000..12900f9 --- /dev/null +++ b/projects/gamilit/apps/database/scripts/QUICK-START.md @@ -0,0 +1,317 @@ +# 🚀 GUÍA RÁPIDA - Scripts de Base de Datos GAMILIT + +**Actualizado:** 2025-11-08 +**Versión:** 3.0 + +--- + +## ⚡ Inicio Rápido + +### Para Desarrollo (Primera Vez) + +```bash +cd /home/isem/workspace/projects/gamilit/apps/database/scripts + +# Inicializar BD completa (crea usuario + BD + DDL + seeds) +./init-database.sh --env dev --force +``` + +### Para Producción (Primera Vez) + +```bash +# Con dotenv-vault (RECOMENDADO) +./manage-secrets.sh generate --env prod +./manage-secrets.sh sync --env prod +./init-database.sh --env prod + +# O con password manual +./init-database.sh --env prod --password "tu_password_seguro_32chars" +``` + +--- + +## 📋 Scripts Disponibles (3 principales) + +### 1. `init-database.sh` - Inicialización Completa ⭐ + +**¿Cuándo usar?** Primera vez, o cuando el usuario NO existe + +```bash +./init-database.sh --env dev # Desarrollo +./init-database.sh --env prod # Producción +./init-database.sh --env dev --force # Sin confirmación +``` + +**¿Qué hace?** +- ✅ Crea usuario `gamilit_user` (si no existe) +- ✅ Genera password seguro de 32 caracteres +- ✅ Crea base de datos `gamilit_platform` +- ✅ Ejecuta DDL (13 schemas, 61 tablas, 61 funciones, 288 índices, 114 RLS policies) +- ✅ Carga seeds del ambiente +- ✅ Actualiza archivos .env automáticamente + +--- + +### 2. `reset-database.sh` - Reset Rápido (Mantiene Usuario) + +**¿Cuándo usar?** Usuario ya existe, solo quieres resetear datos + +```bash +./reset-database.sh --env dev --password "password_existente" +./reset-database.sh --env prod --password "prod_pass" +``` + +**¿Qué hace?** +- ⚠️ Elimina la BD `gamilit_platform` +- ✅ Mantiene el usuario `gamilit_user` (NO cambia password) +- ✅ Recrea BD con DDL y seeds +- ℹ️ NO actualiza .env (credenciales no cambian) + +--- + +### 3. `recreate-database.sh` - Recreación Completa (DESTRUYE TODO) + +**¿Cuándo usar?** Cuando quieres empezar desde cero COMPLETAMENTE + +⚠️ **ADVERTENCIA: ELIMINA USUARIO Y TODOS LOS DATOS** + +```bash +./recreate-database.sh --env dev +./recreate-database.sh --env prod # Requiere confirmación adicional +``` + +**¿Qué hace?** +- ⚠️ Termina todas las conexiones +- ⚠️ Elimina completamente la BD +- ⚠️ Elimina el usuario +- ✅ Ejecuta `init-database.sh` para recrear todo +- ✅ Actualiza archivos .env automáticamente + +--- + +## 🎯 Casos de Uso Comunes + +### Caso 1: Primera vez en proyecto (Setup inicial) + +```bash +./init-database.sh --env dev --force +``` + +### Caso 2: Resetear datos pero mantener usuario + +```bash +# Si conoces el password +./reset-database.sh --env dev --password "mi_password" + +# Si no conoces el password, usa recreate +./recreate-database.sh --env dev +``` + +### Caso 3: Actualizar estructura de BD (nueva migración) + +```bash +# Opción A: Reset rápido (si tienes password) +./reset-database.sh --env dev --password "password" + +# Opción B: Recrear completo (genera nuevo password) +./recreate-database.sh --env dev +``` + +### Caso 4: Aplicar cambios de DDL + +```bash +# Si solo cambiaron DDL/seeds (sin cambios de usuario) +./reset-database.sh --env dev --password "password_actual" +``` + +### Caso 5: Olvidé el password del usuario + +```bash +# Única opción: recrear todo +./recreate-database.sh --env dev +``` + +--- + +## 📊 Comparación Rápida + +| Acción | init-database.sh | reset-database.sh | recreate-database.sh | +|--------|------------------|-------------------|----------------------| +| **Elimina usuario** | ❌ | ❌ | ✅ | +| **Elimina BD** | ⚠️ Si existe | ✅ | ✅ | +| **Crea usuario** | ✅ Si no existe | ❌ | ✅ | +| **Genera password** | ✅ | ❌ | ✅ | +| **Requiere password** | ❌ | ✅ | ❌ | +| **Actualiza .env** | ✅ | ❌ | ✅ | +| **Tiempo aprox** | 30-60s | 20-30s | 40-70s | + +--- + +## 🔑 Gestión de Credenciales + +### ¿Dónde están las credenciales? + +Después de ejecutar `init-database.sh` o `recreate-database.sh`: + +``` +apps/database/database-credentials-{env}.txt ← Credenciales guardadas aquí +``` + +Ejemplo de contenido: +``` +Database Host: localhost +Database Port: 5432 +Database Name: gamilit_platform +Database User: gamilit_user +Database Password: xB9k2mN...Zp8Q +Connection String: postgresql://gamilit_user:xB9k2mN...@localhost:5432/gamilit_platform +``` + +### Archivos .env actualizados automáticamente + +- `apps/backend/.env.{env}` +- `apps/database/.env.{env}` +- `../../gamilit-deployment-scripts/.env.{env}` (si existe) + +--- + +## ⚠️ Advertencias de Seguridad + +### Desarrollo + +- ✅ OK usar `--force` para automatización +- ✅ OK regenerar passwords +- ✅ OK recrear BD frecuentemente + +### Producción + +- ⚠️ NUNCA usar `--force` sin validación +- ⚠️ SIEMPRE hacer backup antes de `recreate-database.sh` +- ⚠️ Confirmar que tienes backup antes de eliminar +- ✅ Usar dotenv-vault para gestión de secrets + +--- + +## 🐛 Troubleshooting + +### Error: "No se puede conectar a PostgreSQL" + +```bash +# Verificar que PostgreSQL está corriendo +sudo systemctl status postgresql + +# O verificar proceso +ps aux | grep postgres + +# Iniciar si está detenido +sudo systemctl start postgresql +``` + +### Error: "Usuario ya existe" + +```bash +# Opción A: Usar reset (si conoces password) +./reset-database.sh --env dev --password "password_existente" + +# Opción B: Recrear todo +./recreate-database.sh --env dev +``` + +### Error: "Base de datos no se puede eliminar (conexiones activas)" + +```bash +# El script ya maneja esto, pero si falla: +sudo -u postgres psql -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname='gamilit_platform';" +``` + +### Error: "Permisos denegados" + +```bash +# Dar permisos de ejecución +chmod +x *.sh + +# Verificar permisos de PostgreSQL +sudo -u postgres psql -c "SELECT version();" +``` + +--- + +## 📁 Estructura de Archivos + +``` +scripts/ +├── init-database.sh ⭐ Script principal (v3.0) +├── reset-database.sh 🔄 Reset rápido +├── recreate-database.sh ⚠️ Recreación completa +├── manage-secrets.sh 🔐 Gestión de secrets +├── update-env-files.sh 🔧 Sincronización .env +├── cleanup-duplicados.sh 🧹 Limpieza +├── QUICK-START.md 📖 Esta guía +├── README.md 📚 Documentación completa +├── deprecated/ 📦 Scripts antiguos +│ ├── init-database-v1.sh +│ ├── init-database-v2.sh +│ └── init-database.sh.backup-* +├── config/ ⚙️ Configuraciones +│ ├── dev.conf +│ └── prod.conf +├── inventory/ 📊 Scripts de inventario +└── utilities/ 🛠️ Utilidades +``` + +--- + +## 🎓 Flujo Recomendado para Nuevos Desarrolladores + +### Día 1 - Setup Inicial + +```bash +# 1. Clonar repositorio +git clone +cd gamilit/projects/gamilit/apps/database/scripts + +# 2. Inicializar BD +./init-database.sh --env dev --force + +# 3. Verificar credenciales +cat ../database-credentials-dev.txt + +# 4. ¡Listo! Backend puede conectarse +``` + +### Día a Día - Desarrollo + +```bash +# Aplicar cambios de DDL +./reset-database.sh --env dev --password "$(grep 'Database Password' ../database-credentials-dev.txt | cut -d: -f2 | xargs)" + +# O más simple: recrear todo +./recreate-database.sh --env dev --force +``` + +--- + +## ✅ Checklist Pre-Deployment Producción + +- [ ] Backup completo de BD actual +- [ ] Verificar que `manage-secrets.sh` tiene secrets generados +- [ ] Probar script en staging primero +- [ ] Tener plan de rollback +- [ ] Notificar al equipo del deployment +- [ ] Ejecutar con --env prod (SIN --force) +- [ ] Validar conexiones post-deployment +- [ ] Verificar que seeds de producción se cargaron + +--- + +## 📞 Soporte + +- **Documentación completa:** `README.md` +- **Scripts de inventario:** `inventory/` +- **Logs:** Revisa output del script (se muestra en consola) + +--- + +**Última actualización:** 2025-11-08 +**Versión de scripts:** 3.0 +**Mantenido por:** Equipo de Base de Datos GAMILIT diff --git a/projects/gamilit/apps/database/scripts/testing/CREAR-USUARIOS-TESTING.sql b/projects/gamilit/apps/database/scripts/testing/CREAR-USUARIOS-TESTING.sql new file mode 100644 index 0000000..9badb82 --- /dev/null +++ b/projects/gamilit/apps/database/scripts/testing/CREAR-USUARIOS-TESTING.sql @@ -0,0 +1,395 @@ +-- ===================================================== +-- SCRIPT DE EMERGENCIA: CREAR USUARIOS DE TESTING +-- ===================================================== +-- Fecha: 2025-11-11 +-- Propósito: Crear usuarios de testing manualmente +-- Usuarios: admin@gamilit.com, teacher@gamilit.com, student@gamilit.com +-- Password: Test1234 (para todos) +-- ===================================================== + +-- Habilitar extensión pgcrypto si no está habilitada +CREATE EXTENSION IF NOT EXISTS pgcrypto; + +-- Verificar tenant por defecto +DO $$ +DECLARE + default_tenant_id uuid; +BEGIN + -- Buscar o crear tenant por defecto + SELECT id INTO default_tenant_id + FROM auth_management.tenants + WHERE name = 'GAMILIT Platform' + LIMIT 1; + + IF default_tenant_id IS NULL THEN + INSERT INTO auth_management.tenants ( + id, name, slug, status, settings, created_at, updated_at + ) VALUES ( + '00000000-0000-0000-0000-000000000001'::uuid, + 'GAMILIT Platform', + 'gamilit-platform', + 'active', + '{}'::jsonb, + NOW(), + NOW() + ) ON CONFLICT (id) DO NOTHING; + + default_tenant_id := '00000000-0000-0000-0000-000000000001'::uuid; + END IF; + + RAISE NOTICE 'Tenant ID: %', default_tenant_id; +END $$; + +-- ===================================================== +-- PASO 1: CREAR USUARIOS EN auth.users +-- ===================================================== + +INSERT INTO auth.users ( + id, + instance_id, + email, + encrypted_password, + email_confirmed_at, + raw_app_meta_data, + raw_user_meta_data, + gamilit_role, + created_at, + updated_at, + confirmation_token, + email_change, + email_change_token_new, + recovery_token +) VALUES +-- ADMIN +( + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid, + '00000000-0000-0000-0000-000000000000'::uuid, + 'admin@gamilit.com', + crypt('Test1234', gen_salt('bf', 10)), + NOW(), + jsonb_build_object( + 'provider', 'email', + 'providers', ARRAY['email'] + ), + jsonb_build_object( + 'name', 'Admin GAMILIT', + 'role', 'super_admin' + ), + 'super_admin'::auth_management.gamilit_role, + NOW(), + NOW(), + '', + '', + '', + '' +), +-- TEACHER +( + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid, + '00000000-0000-0000-0000-000000000000'::uuid, + 'teacher@gamilit.com', + crypt('Test1234', gen_salt('bf', 10)), + NOW(), + jsonb_build_object( + 'provider', 'email', + 'providers', ARRAY['email'] + ), + jsonb_build_object( + 'name', 'Profesor Testing', + 'role', 'teacher' + ), + 'teacher'::auth_management.gamilit_role, + NOW(), + NOW(), + '', + '', + '', + '' +), +-- STUDENT +( + 'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid, + '00000000-0000-0000-0000-000000000000'::uuid, + 'student@gamilit.com', + crypt('Test1234', gen_salt('bf', 10)), + NOW(), + jsonb_build_object( + 'provider', 'email', + 'providers', ARRAY['email'] + ), + jsonb_build_object( + 'name', 'Estudiante Testing', + 'role', 'student' + ), + 'student'::auth_management.gamilit_role, + NOW(), + NOW(), + '', + '', + '', + '' +) +ON CONFLICT (email) DO UPDATE SET + encrypted_password = EXCLUDED.encrypted_password, + updated_at = NOW(); + +-- ===================================================== +-- PASO 2: CREAR PROFILES EN auth_management.profiles +-- ===================================================== + +INSERT INTO auth_management.profiles ( + id, + user_id, + tenant_id, + email, + full_name, + first_name, + last_name, + role, + status, + email_verified, + preferences, + created_at, + updated_at +) VALUES +-- ADMIN PROFILE +( + 'aaaaaaaa-aaaa-aaaa-bbbb-aaaaaaaaaaaa'::uuid, + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid, + 'admin@gamilit.com', + 'Admin GAMILIT', + 'Admin', + 'GAMILIT', + 'super_admin'::auth_management.gamilit_role, + 'active'::auth_management.user_status, + true, + jsonb_build_object( + 'theme', 'detective', + 'language', 'es', + 'timezone', 'America/Mexico_City', + 'sound_enabled', true, + 'notifications_enabled', true + ), + NOW(), + NOW() +), +-- TEACHER PROFILE +( + 'bbbbbbbb-bbbb-bbbb-cccc-bbbbbbbbbbbb'::uuid, + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid, + 'teacher@gamilit.com', + 'Profesor Testing', + 'Profesor', + 'Testing', + 'teacher'::auth_management.gamilit_role, + 'active'::auth_management.user_status, + true, + jsonb_build_object( + 'theme', 'detective', + 'language', 'es', + 'timezone', 'America/Mexico_City', + 'sound_enabled', true, + 'notifications_enabled', true + ), + NOW(), + NOW() +), +-- STUDENT PROFILE +( + 'cccccccc-cccc-cccc-dddd-cccccccccccc'::uuid, + 'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid, + 'student@gamilit.com', + 'Estudiante Testing', + 'Estudiante', + 'Testing', + 'student'::auth_management.gamilit_role, + 'active'::auth_management.user_status, + true, + jsonb_build_object( + 'theme', 'detective', + 'language', 'es', + 'timezone', 'America/Mexico_City', + 'sound_enabled', true, + 'notifications_enabled', true, + 'grade_level', '5' + ), + NOW(), + NOW() +) +ON CONFLICT (user_id) DO UPDATE SET + full_name = EXCLUDED.full_name, + updated_at = NOW(); + +-- ===================================================== +-- PASO 3: INICIALIZAR user_stats (gamification) +-- ===================================================== +-- Nota: El trigger trg_initialize_user_stats debería hacer esto automáticamente +-- pero lo agregamos manualmente por si acaso + +INSERT INTO gamification_system.user_stats ( + id, + user_id, + tenant_id, + level, + total_xp, + xp_to_next_level, + current_rank, + ml_coins, + ml_coins_earned_total, + created_at, + updated_at +) VALUES +( + 'aaaaaaaa-aaaa-stat-aaaa-aaaaaaaaaaaa'::uuid, + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid, + 1, + 0, + 100, + 'Ajaw'::gamification_system.maya_rank, + 100, + 100, + NOW(), + NOW() +), +( + 'bbbbbbbb-bbbb-stat-bbbb-bbbbbbbbbbbb'::uuid, + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid, + 1, + 0, + 100, + 'Ajaw'::gamification_system.maya_rank, + 100, + 100, + NOW(), + NOW() +), +( + 'cccccccc-cccc-stat-cccc-cccccccccccc'::uuid, + 'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid, + 1, + 0, + 100, + 'Ajaw'::gamification_system.maya_rank, + 100, + 100, + NOW(), + NOW() +) +ON CONFLICT (user_id) DO UPDATE SET + updated_at = NOW(); + +-- ===================================================== +-- PASO 4: INICIALIZAR user_ranks (gamification) +-- ===================================================== + +INSERT INTO gamification_system.user_ranks ( + id, + user_id, + tenant_id, + current_rank, + rank_level, + total_rank_points, + rank_achieved_at, + created_at, + updated_at +) VALUES +( + 'aaaaaaaa-aaaa-rank-aaaa-aaaaaaaaaaaa'::uuid, + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid, + 'Ajaw'::gamification_system.maya_rank, + 1, + 0, + NOW(), + NOW(), + NOW() +), +( + 'bbbbbbbb-bbbb-rank-bbbb-bbbbbbbbbbbb'::uuid, + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid, + 'Ajaw'::gamification_system.maya_rank, + 1, + 0, + NOW(), + NOW(), + NOW() +), +( + 'cccccccc-cccc-rank-cccc-cccccccccccc'::uuid, + 'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid, + '00000000-0000-0000-0000-000000000001'::uuid, + 'Ajaw'::gamification_system.maya_rank, + 1, + 0, + NOW(), + NOW(), + NOW() +) +ON CONFLICT (user_id) DO UPDATE SET + updated_at = NOW(); + +-- ===================================================== +-- VERIFICACIÓN FINAL +-- ===================================================== + +DO $$ +DECLARE + users_count INTEGER; + profiles_count INTEGER; + stats_count INTEGER; + ranks_count INTEGER; +BEGIN + SELECT COUNT(*) INTO users_count FROM auth.users + WHERE email IN ('admin@gamilit.com', 'teacher@gamilit.com', 'student@gamilit.com'); + + SELECT COUNT(*) INTO profiles_count FROM auth_management.profiles + WHERE email IN ('admin@gamilit.com', 'teacher@gamilit.com', 'student@gamilit.com'); + + SELECT COUNT(*) INTO stats_count FROM gamification_system.user_stats + WHERE user_id IN ( + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid, + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid, + 'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid + ); + + SELECT COUNT(*) INTO ranks_count FROM gamification_system.user_ranks + WHERE user_id IN ( + 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'::uuid, + 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'::uuid, + 'cccccccc-cccc-cccc-cccc-cccccccccccc'::uuid + ); + + RAISE NOTICE '========================================'; + RAISE NOTICE 'USUARIOS DE TESTING CREADOS'; + RAISE NOTICE '========================================'; + RAISE NOTICE 'auth.users: % usuarios', users_count; + RAISE NOTICE 'auth_management.profiles: % profiles', profiles_count; + RAISE NOTICE 'gamification_system.user_stats: % stats', stats_count; + RAISE NOTICE 'gamification_system.user_ranks: % ranks', ranks_count; + RAISE NOTICE '========================================'; + + IF users_count = 3 AND profiles_count = 3 AND stats_count = 3 AND ranks_count = 3 THEN + RAISE NOTICE '✅ TODOS LOS USUARIOS CREADOS EXITOSAMENTE'; + RAISE NOTICE ''; + RAISE NOTICE 'Credenciales de testing:'; + RAISE NOTICE ' - admin@gamilit.com / Test1234'; + RAISE NOTICE ' - teacher@gamilit.com / Test1234'; + RAISE NOTICE ' - student@gamilit.com / Test1234'; + ELSE + RAISE WARNING '⚠️ ALGUNOS USUARIOS NO SE CREARON CORRECTAMENTE'; + RAISE WARNING 'Esperado: 3 users, 3 profiles, 3 stats, 3 ranks'; + RAISE WARNING 'Creado: % users, % profiles, % stats, % ranks', + users_count, profiles_count, stats_count, ranks_count; + END IF; +END $$; + +-- ===================================================== +-- FIN DEL SCRIPT +-- ===================================================== diff --git a/projects/gamilit/apps/database/scripts/validations/README.md b/projects/gamilit/apps/database/scripts/validations/README.md new file mode 100644 index 0000000..33c0000 --- /dev/null +++ b/projects/gamilit/apps/database/scripts/validations/README.md @@ -0,0 +1,205 @@ +# Scripts de Validación de Integridad - GAMILIT Database + +**Fecha:** 2025-11-24 +**Mantenido por:** Database-Agent +**Propósito:** Scripts para validar y mantener la integridad de datos de XP y ML Coins + +--- + +## 📋 Scripts Disponibles + +### 1. `quick-validate-xp.sql` + +**Descripción:** Validación rápida (30 segundos) para detectar problemas de integridad en XP. + +**Uso:** +```bash +cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit/apps/database +PGPASSWORD='C5hq7253pdVyVKUC' psql -h localhost -U gamilit_user -d gamilit_platform -f scripts/quick-validate-xp.sql +``` + +**Salida esperada (sistema saludable):** +``` +1. Intentos con score > 0 pero xp_earned = 0: + intentos_problematicos = 0 + +2. Usuarios con discrepancias: + usuarios_con_discrepancias = 0 + +3. Estado de integridad: + estado = "✅ INTEGRIDAD OK" +``` + +**Frecuencia recomendada:** Diaria o después de cada deployment + +--- + +### 2. `validate-xp-integrity.sql` + +**Descripción:** Validación completa (2-3 minutos) con reporte detallado de todos los aspectos de integridad. + +**Uso:** +```bash +cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit/apps/database +PGPASSWORD='C5hq7253pdVyVKUC' psql -h localhost -U gamilit_user -d gamilit_platform -f scripts/validate-xp-integrity.sql +``` + +**Validaciones incluidas:** +1. Intentos con score > 0 pero xp_earned = 0 +2. Usuarios con discrepancias entre attempts y user_stats +3. Intentos donde xp_earned no coincide con la fórmula esperada +4. User stats sin attempts registrados +5. Resumen general del sistema + +**Frecuencia recomendada:** Semanal o cuando se detecten anomalías + +--- + +### 3. `fix-historical-xp-ml-coins-v2.sql` + +**Descripción:** Script de corrección automática de datos históricos (solo si se detectan problemas). + +**⚠️ ADVERTENCIA:** Solo ejecutar si `quick-validate-xp.sql` reporta problemas. + +**Uso:** +```bash +cd /home/isem/workspace/workspace-gamilit/gamilit/projects/gamilit/apps/database +PGPASSWORD='C5hq7253pdVyVKUC' psql -h localhost -U gamilit_user -d gamilit_platform -f scripts/fix-historical-xp-ml-coins-v2.sql + +# Revisar output cuidadosamente antes de confirmar +# Si todo se ve bien, ejecutar en otra sesión: +PGPASSWORD='C5hq7253pdVyVKUC' psql -h localhost -U gamilit_user -d gamilit_platform -c "COMMIT;" +``` + +**Acciones que realiza:** +1. Deshabilita trigger `trg_check_rank_promotion_on_xp_gain` +2. Corrige `xp_earned` y `ml_coins_earned` en `exercise_attempts` +3. Recalcula `user_stats` basado en suma de attempts +4. Rehabilita trigger +5. Valida integridad final + +**Frecuencia recomendada:** Solo cuando sea necesario (no es una tarea periódica) + +--- + +## 🔍 Interpretación de Resultados + +### Estado: ✅ INTEGRIDAD OK + +Todo funciona correctamente. No se requiere acción. + +### Estado: ❌ HAY PROBLEMAS + +**Pasos a seguir:** + +1. **Ejecutar validación completa:** + ```bash + psql -d gamilit_platform -f scripts/validate-xp-integrity.sql + ``` + +2. **Analizar reporte detallado:** + - ¿Cuántos intentos afectados? + - ¿Cuántos usuarios tienen discrepancias? + - ¿Cuál es la magnitud del problema? + +3. **Si hay pocos casos aislados (< 5 usuarios):** + - Ejecutar script de corrección automática + - Revisar logs antes de confirmar + - Validar resultado + +4. **Si hay muchos casos (> 5 usuarios):** + - NO ejecutar script automático + - Investigar la causa raíz + - Consultar con Database-Agent o Tech Lead + +--- + +## 📊 Fórmulas de XP y ML Coins + +### XP Earned +```sql +xp_earned = GREATEST(0, score - (hints_used * 10)) +``` + +**Ejemplos:** +- Score: 100, hints: 0 → XP: 100 +- Score: 100, hints: 2 → XP: 80 +- Score: 50, hints: 10 → XP: 0 (no negativo) + +### ML Coins Earned +```sql +ml_coins_earned = GREATEST(0, FLOOR(score / 10) - (comodines_used * 2)) +``` + +**Ejemplos:** +- Score: 100, comodines: 0 → ML Coins: 10 +- Score: 100, comodines: 2 → ML Coins: 6 +- Score: 50, comodines: 0 → ML Coins: 5 + +--- + +## 🚨 Problemas Comunes + +### 1. Intentos con xp_earned = 0 + +**Causa:** Bug en el código que crea el attempt sin calcular XP. + +**Solución:** Ejecutar script de corrección automática. + +**Prevención:** Agregar validación en backend antes de insertar attempt. + +### 2. Discrepancia entre attempts y user_stats + +**Causa:** Trigger `update_user_stats_on_exercise_complete` no se ejecutó correctamente. + +**Solución:** Recalcular user_stats con script de corrección. + +**Prevención:** Monitorear logs de triggers. + +### 3. Fórmulas inconsistentes + +**Causa:** Cambio en lógica de negocio sin migración de datos históricos. + +**Solución:** Decidir si mantener datos históricos o migrar. + +**Prevención:** Documentar cambios en fórmulas. + +--- + +## 📝 Historial de Correcciones + +### 2025-11-24: Corrección inicial de datos históricos + +- **Usuario afectado:** `85a2d456-a07d-4be9-b9ce-4a46b183a2a0` +- **Intentos corregidos:** 1 +- **XP recuperado:** +600 XP (500 → 1100) +- **ML Coins recuperados:** +90 ML Coins (220 → 310) +- **Bug adicional:** Corregida función `promote_to_next_rank()` + +**Ver detalles completos:** +- `/apps/database/REPORTE-CORRECCION-XP-ML-COINS-2025-11-24.md` +- `/apps/database/RESUMEN-EJECUTIVO-CORRECCION-XP-2025-11-24.md` + +--- + +## 🔗 Referencias + +- **Política DDL-First:** `/orchestration/directivas/DIRECTIVA-POLITICA-CARGA-LIMPIA.md` +- **Traza de tareas:** `/orchestration/trazas/TRAZA-TAREAS-DATABASE.md` +- **Prompt Database-Agent:** `/orchestration/prompts/PROMPT-DATABASE-AGENT.md` + +--- + +## 📞 Contacto + +**Mantenido por:** Database-Agent +**Última actualización:** 2025-11-24 + +Para preguntas o problemas, consultar: +1. Documentación en `/apps/database/docs/` +2. Trazas en `/orchestration/trazas/` +3. Tech Lead del proyecto GAMILIT + +--- + +**FIN DEL README** diff --git a/projects/gamilit/apps/database/scripts/validations/VALIDACIONES-RAPIDAS-POST-RECREACION.sql b/projects/gamilit/apps/database/scripts/validations/VALIDACIONES-RAPIDAS-POST-RECREACION.sql new file mode 100644 index 0000000..23e47fb --- /dev/null +++ b/projects/gamilit/apps/database/scripts/validations/VALIDACIONES-RAPIDAS-POST-RECREACION.sql @@ -0,0 +1,219 @@ +-- ============================================================================ +-- VALIDACIONES RAPIDAS POST-RECREACION DE BASE DE DATOS +-- Fecha: 2025-11-24 +-- Database: gamilit_platform +-- ============================================================================ +-- +-- Este archivo contiene queries SQL para validar rapidamente la integridad +-- de la base de datos despues de una recreacion. +-- +-- Uso: +-- psql -h localhost -U gamilit_user -d gamilit_platform -f VALIDACIONES-RAPIDAS-POST-RECREACION.sql +-- ============================================================================ + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 1: CONTEO DE SCHEMAS' +\echo '============================================================================' +SELECT count(*) as total_schemas +FROM information_schema.schemata +WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast'); + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 2: CONTEO DE TABLAS POR SCHEMA' +\echo '============================================================================' +SELECT table_schema, count(*) as tables +FROM information_schema.tables +WHERE table_schema NOT IN ('pg_catalog', 'information_schema', 'pg_toast') + AND table_type = 'BASE TABLE' +GROUP BY table_schema +ORDER BY table_schema; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 3: CONTEO DE FUNCIONES' +\echo '============================================================================' +SELECT count(*) as total_functions +FROM information_schema.routines +WHERE routine_schema NOT IN ('pg_catalog', 'information_schema'); + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 4: CONTEO DE TRIGGERS' +\echo '============================================================================' +SELECT count(DISTINCT trigger_name) as total_triggers +FROM information_schema.triggers; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 5: RLS POLICIES EN GAMIFICATION_SYSTEM.NOTIFICATIONS' +\echo '============================================================================' +SELECT policyname, cmd +FROM pg_policies +WHERE tablename = 'notifications' + AND schemaname = 'gamification_system' +ORDER BY policyname; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 6: RLS POLICIES EN STUDENT_INTERVENTION_ALERTS' +\echo '============================================================================' +SELECT policyname, cmd +FROM pg_policies +WHERE tablename = 'student_intervention_alerts' + AND schemaname = 'progress_tracking' +ORDER BY policyname; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 7: CONTEO DE USUARIOS POR ROL' +\echo '============================================================================' +SELECT + role, + count(*) as total +FROM auth_management.profiles +GROUP BY role +ORDER BY role; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 8: MODULOS EDUCATIVOS' +\echo '============================================================================' +SELECT + id, + title, + status, + (SELECT count(*) FROM educational_content.exercises e WHERE e.module_id = m.id) as ejercicios +FROM educational_content.modules m +ORDER BY title; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 9: RANGOS MAYA' +\echo '============================================================================' +SELECT + rank_name, + display_name, + min_xp_required, + max_xp_threshold, + ml_coins_bonus, + is_active +FROM gamification_system.maya_ranks +ORDER BY min_xp_required; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 10: USER STATS INICIALIZADOS' +\echo '============================================================================' +SELECT + count(*) as total_users, + count(*) FILTER (WHERE total_xp > 0) as con_xp, + count(*) FILTER (WHERE ml_coins > 0) as con_coins, + sum(total_xp)::bigint as total_xp_sistema, + sum(ml_coins)::bigint as total_coins_sistema +FROM gamification_system.user_stats; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 11: MODULE PROGRESS INICIALIZADO' +\echo '============================================================================' +SELECT + count(*) as total_records, + count(DISTINCT user_id) as usuarios, + count(DISTINCT module_id) as modulos, + count(*) FILTER (WHERE status = 'completed') as completados, + count(*) FILTER (WHERE status = 'in_progress') as en_progreso, + count(*) FILTER (WHERE status = 'not_started') as no_iniciados +FROM progress_tracking.module_progress; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 12: SOCIAL FEATURES' +\echo '============================================================================' +SELECT 'Schools' as tipo, count(*)::text as total FROM social_features.schools +UNION ALL +SELECT 'Classrooms', count(*)::text FROM social_features.classrooms +UNION ALL +SELECT 'Teacher-Classroom Links', count(*)::text FROM social_features.teacher_classrooms; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 13: RESUMEN DE OBJETOS POR SCHEMA' +\echo '============================================================================' +SELECT + s.schema_name, + COALESCE(t.tables, 0) as tables, + COALESCE(v.views, 0) as views, + COALESCE(f.functions, 0) as functions, + COALESCE(tr.triggers, 0) as triggers, + COALESCE(p.policies, 0) as rls_policies +FROM ( + SELECT schema_name + FROM information_schema.schemata + WHERE schema_name NOT IN ('pg_catalog', 'information_schema', 'pg_toast', 'pg_temp_3', 'pg_toast_temp_3') +) s +LEFT JOIN ( + SELECT table_schema, count(*) as tables + FROM information_schema.tables + WHERE table_type = 'BASE TABLE' + GROUP BY table_schema +) t ON s.schema_name = t.table_schema +LEFT JOIN ( + SELECT table_schema, count(*) as views + FROM information_schema.views + GROUP BY table_schema +) v ON s.schema_name = v.table_schema +LEFT JOIN ( + SELECT routine_schema, count(*) as functions + FROM information_schema.routines + GROUP BY routine_schema +) f ON s.schema_name = f.routine_schema +LEFT JOIN ( + SELECT trigger_schema, count(DISTINCT trigger_name) as triggers + FROM information_schema.triggers + GROUP BY trigger_schema +) tr ON s.schema_name = tr.trigger_schema +LEFT JOIN ( + SELECT schemaname, count(*) as policies + FROM pg_policies + GROUP BY schemaname +) p ON s.schema_name = p.schemaname +WHERE s.schema_name IN ( + 'admin_dashboard', + 'audit_logging', + 'auth', + 'auth_management', + 'communication', + 'content_management', + 'educational_content', + 'gamification_system', + 'gamilit', + 'lti_integration', + 'notifications', + 'progress_tracking', + 'social_features', + 'system_configuration' +) +ORDER BY s.schema_name; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION 14: TABLAS CON RLS HABILITADO' +\echo '============================================================================' +SELECT + schemaname, + tablename, + rowsecurity as rls_enabled +FROM pg_tables +WHERE schemaname NOT IN ('pg_catalog', 'information_schema', 'pg_toast') + AND rowsecurity = true +ORDER BY schemaname, tablename; + +\echo '' +\echo '============================================================================' +\echo 'VALIDACION COMPLETA' +\echo '============================================================================' +\echo 'Si todas las validaciones anteriores se ejecutaron sin errores,' +\echo 'la base de datos esta correctamente recreada y operacional.' +\echo '' diff --git a/projects/gamilit/apps/database/scripts/validations/validate-gap-fixes.sql b/projects/gamilit/apps/database/scripts/validations/validate-gap-fixes.sql new file mode 100644 index 0000000..a80187f --- /dev/null +++ b/projects/gamilit/apps/database/scripts/validations/validate-gap-fixes.sql @@ -0,0 +1,234 @@ +-- ===================================================== +-- Script de Validación: Gaps Database-Backend +-- Fecha: 2025-11-24 +-- Tarea: DB-127 Corrección Gaps Coherencia +-- ===================================================== +-- +-- Este script valida que los 3 gaps identificados estén resueltos: +-- - GAP-DB-001: activity_log con entity_type, entity_id +-- - GAP-DB-002: auth.tenants vista alias +-- - GAP-DB-003: classrooms.is_deleted +-- +-- ===================================================== + +\echo '===================================================' +\echo 'VALIDACIÓN DE GAPS DATABASE-BACKEND' +\echo '===================================================' +\echo '' + +-- ===================================================== +-- GAP-DB-001: Validar tabla activity_log +-- ===================================================== + +\echo '--- GAP-DB-001: Tabla audit_logging.activity_log ---' +\echo '' + +-- Validar que tabla existe +SELECT + CASE + WHEN EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_schema = 'audit_logging' + AND table_name = 'activity_log' + ) + THEN '✅ Tabla audit_logging.activity_log existe' + ELSE '❌ ERROR: Tabla audit_logging.activity_log NO existe' + END as status; + +-- Validar columnas requeridas +\echo '' +\echo 'Validar columnas en activity_log:' +SELECT + column_name, + data_type, + is_nullable, + column_default, + CASE + WHEN column_name IN ('id', 'user_id', 'action_type', 'entity_type', 'entity_id', 'description', 'metadata', 'created_at') + THEN '✅ Requerida' + ELSE ' Opcional' + END as importancia +FROM information_schema.columns +WHERE table_schema = 'audit_logging' +AND table_name = 'activity_log' +ORDER BY ordinal_position; + +-- Validar indices +\echo '' +\echo 'Validar índices en activity_log:' +SELECT + indexname, + indexdef +FROM pg_indexes +WHERE schemaname = 'audit_logging' +AND tablename = 'activity_log' +ORDER BY indexname; + +-- Validar query backend (query de admin-dashboard.service.ts:184) +\echo '' +\echo 'Validar query backend - Actividad por tipo (últimos 7 días):' +SELECT + action_type, + COUNT(*) as count +FROM audit_logging.activity_log +WHERE created_at > NOW() - INTERVAL '7 days' +GROUP BY action_type +ORDER BY count DESC +LIMIT 5; + +-- ===================================================== +-- GAP-DB-002: Validar vista auth.tenants +-- ===================================================== + +\echo '' +\echo '--- GAP-DB-002: Vista auth.tenants (alias) ---' +\echo '' + +-- Validar que vista existe +SELECT + CASE + WHEN EXISTS ( + SELECT 1 FROM information_schema.views + WHERE table_schema = 'auth' + AND table_name = 'tenants' + ) + THEN '✅ Vista auth.tenants existe' + ELSE '❌ ERROR: Vista auth.tenants NO existe' + END as status; + +-- Validar query backend (query de admin-dashboard.service.ts:95) +\echo '' +\echo 'Validar query backend - Tenants actualizados (últimos 7 días):' +SELECT + id, + name, + slug, + updated_at +FROM auth.tenants +WHERE updated_at >= NOW() - INTERVAL '7 days' +ORDER BY updated_at DESC +LIMIT 3; + +-- Validar que apunta a auth_management.tenants +\echo '' +\echo 'Validar definición de vista:' +SELECT + schemaname, + viewname, + LEFT(definition, 100) || '...' as definition_preview +FROM pg_views +WHERE schemaname = 'auth' +AND viewname = 'tenants'; + +-- ===================================================== +-- GAP-DB-003: Validar classrooms.is_deleted +-- ===================================================== + +\echo '' +\echo '--- GAP-DB-003: Columna classrooms.is_deleted ---' +\echo '' + +-- Validar que columna existe +SELECT + CASE + WHEN EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_schema = 'social_features' + AND table_name = 'classrooms' + AND column_name = 'is_deleted' + ) + THEN '✅ Columna social_features.classrooms.is_deleted existe' + ELSE '❌ ERROR: Columna is_deleted NO existe en classrooms' + END as status; + +-- Validar tipo y default de columna +\echo '' +\echo 'Validar columna is_deleted:' +SELECT + column_name, + data_type, + is_nullable, + column_default +FROM information_schema.columns +WHERE table_schema = 'social_features' +AND table_name = 'classrooms' +AND column_name IN ('is_deleted', 'is_archived', 'is_active') +ORDER BY column_name; + +-- Validar índice parcial para is_deleted +\echo '' +\echo 'Validar índice parcial para is_deleted:' +SELECT + indexname, + indexdef +FROM pg_indexes +WHERE schemaname = 'social_features' +AND tablename = 'classrooms' +AND indexdef LIKE '%is_deleted%' +ORDER BY indexname; + +-- Validar query backend (query de classrooms.service.ts:67) +\echo '' +\echo 'Validar query backend - Classrooms no eliminados:' +SELECT + id, + name, + code, + is_active, + is_archived, + is_deleted +FROM social_features.classrooms +WHERE is_deleted = FALSE +ORDER BY created_at DESC +LIMIT 3; + +-- ===================================================== +-- RESUMEN FINAL +-- ===================================================== + +\echo '' +\echo '===================================================' +\echo 'RESUMEN DE VALIDACIÓN' +\echo '===================================================' +\echo '' + +SELECT + '✅ GAP-DB-001: activity_log' as gap, + CASE + WHEN COUNT(*) >= 8 THEN '✅ RESUELTO' + ELSE '❌ PENDIENTE' + END as estado +FROM information_schema.columns +WHERE table_schema = 'audit_logging' +AND table_name = 'activity_log' +AND column_name IN ('id', 'user_id', 'action_type', 'entity_type', 'entity_id', 'description', 'metadata', 'created_at') + +UNION ALL + +SELECT + '✅ GAP-DB-002: auth.tenants' as gap, + CASE + WHEN COUNT(*) = 1 THEN '✅ RESUELTO' + ELSE '❌ PENDIENTE' + END as estado +FROM information_schema.views +WHERE table_schema = 'auth' +AND table_name = 'tenants' + +UNION ALL + +SELECT + '✅ GAP-DB-003: classrooms.is_deleted' as gap, + CASE + WHEN COUNT(*) = 1 THEN '✅ RESUELTO' + ELSE '❌ PENDIENTE' + END as estado +FROM information_schema.columns +WHERE table_schema = 'social_features' +AND table_name = 'classrooms' +AND column_name = 'is_deleted'; + +\echo '' +\echo '===================================================' +\echo 'VALIDACIÓN COMPLETADA' +\echo '===================================================' diff --git a/projects/gamilit/apps/database/scripts/validations/validate-generate-alerts-joins.sql b/projects/gamilit/apps/database/scripts/validations/validate-generate-alerts-joins.sql new file mode 100644 index 0000000..1c72c59 --- /dev/null +++ b/projects/gamilit/apps/database/scripts/validations/validate-generate-alerts-joins.sql @@ -0,0 +1,253 @@ +-- ===================================================== +-- Script de Validación: generate_student_alerts() JOINs +-- Descripción: Valida que los JOINs arquitectónicos sean correctos +-- Fecha: 2025-11-24 +-- Agente: Database-Agent +-- ===================================================== + +\echo '=========================================' +\echo 'VALIDACIÓN: generate_student_alerts()' +\echo 'Verificando JOINs arquitectónicos' +\echo '=========================================' +\echo '' + +-- ===================================================== +-- 1. VERIFICAR QUE LA FUNCIÓN EXISTE +-- ===================================================== +\echo '1. Verificando que la función existe...' +SELECT + p.proname as function_name, + n.nspname as schema_name, + pg_get_functiondef(p.oid) LIKE '%auth_management.profiles%' as uses_profiles_join, + pg_get_functiondef(p.oid) LIKE '%JOIN auth.users%' as uses_auth_users_join +FROM pg_proc p +JOIN pg_namespace n ON p.pronamespace = n.oid +WHERE p.proname = 'generate_student_alerts' + AND n.nspname = 'progress_tracking'; + +\echo '' +\echo ' ✓ Si uses_profiles_join = t y uses_auth_users_join = f → CORRECTO' +\echo ' ✗ Si uses_auth_users_join = t → INCORRECTO (todavía usa JOINs antiguos)' +\echo '' + +-- ===================================================== +-- 2. VERIFICAR FOREIGN KEYS RELEVANTES +-- ===================================================== +\echo '2. Verificando Foreign Keys relevantes...' +\echo '' + +\echo ' 2.1 module_progress.user_id → profiles(id)' +SELECT + tc.table_schema, + tc.table_name, + kcu.column_name, + ccu.table_schema AS foreign_table_schema, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name +FROM information_schema.table_constraints AS tc +JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema +JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema +WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = 'progress_tracking' + AND tc.table_name = 'module_progress' + AND kcu.column_name = 'user_id'; + +\echo '' +\echo ' 2.2 exercise_submissions.user_id → profiles(id)' +SELECT + tc.table_schema, + tc.table_name, + kcu.column_name, + ccu.table_schema AS foreign_table_schema, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name +FROM information_schema.table_constraints AS tc +JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema +JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema +WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = 'progress_tracking' + AND tc.table_name = 'exercise_submissions' + AND kcu.column_name = 'user_id'; + +\echo '' +\echo ' 2.3 student_intervention_alerts.student_id → auth.users(id)' +SELECT + tc.table_schema, + tc.table_name, + kcu.column_name, + ccu.table_schema AS foreign_table_schema, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name +FROM information_schema.table_constraints AS tc +JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema +JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema +WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = 'progress_tracking' + AND tc.table_name = 'student_intervention_alerts' + AND kcu.column_name = 'student_id'; + +\echo '' +\echo ' 2.4 profiles.user_id → auth.users(id)' +SELECT + tc.table_schema, + tc.table_name, + kcu.column_name, + ccu.table_schema AS foreign_table_schema, + ccu.table_name AS foreign_table_name, + ccu.column_name AS foreign_column_name +FROM information_schema.table_constraints AS tc +JOIN information_schema.key_column_usage AS kcu + ON tc.constraint_name = kcu.constraint_name + AND tc.table_schema = kcu.table_schema +JOIN information_schema.constraint_column_usage AS ccu + ON ccu.constraint_name = tc.constraint_name + AND ccu.table_schema = tc.table_schema +WHERE tc.constraint_type = 'FOREIGN KEY' + AND tc.table_schema = 'auth_management' + AND tc.table_name = 'profiles' + AND kcu.column_name = 'user_id'; + +-- ===================================================== +-- 3. RECREAR LA FUNCIÓN ACTUALIZADA +-- ===================================================== +\echo '' +\echo '3. Recreando la función con JOINs corregidos...' +\i apps/database/ddl/schemas/progress_tracking/functions/15-generate_student_alerts.sql + +-- ===================================================== +-- 4. VERIFICAR DEFINICIÓN DE LA FUNCIÓN +-- ===================================================== +\echo '' +\echo '4. Verificando definición de la función...' +\echo '' + +-- Contar ocurrencias de JOIN auth_management.profiles +\echo ' 4.1 Ocurrencias de "JOIN auth_management.profiles":' +SELECT COUNT(*)::text || ' ocurrencias (esperado: 3)' as resultado +FROM regexp_matches( + pg_get_functiondef( + (SELECT oid FROM pg_proc WHERE proname = 'generate_student_alerts' AND pronamespace = 'progress_tracking'::regnamespace) + ), + 'JOIN auth_management\.profiles', + 'g' +); + +-- Contar ocurrencias de JOIN auth.users (debería ser 0) +\echo '' +\echo ' 4.2 Ocurrencias de "JOIN auth.users" (debe ser 0):' +SELECT COALESCE(COUNT(*)::text, '0') || ' ocurrencias (esperado: 0)' as resultado +FROM regexp_matches( + pg_get_functiondef( + (SELECT oid FROM pg_proc WHERE proname = 'generate_student_alerts' AND pronamespace = 'progress_tracking'::regnamespace) + ), + 'JOIN auth\.users', + 'g' +); + +-- Contar ocurrencias de p.tenant_id +\echo '' +\echo ' 4.3 Ocurrencias de "p.tenant_id":' +SELECT COUNT(*)::text || ' ocurrencias (esperado: 3)' as resultado +FROM regexp_matches( + pg_get_functiondef( + (SELECT oid FROM pg_proc WHERE proname = 'generate_student_alerts' AND pronamespace = 'progress_tracking'::regnamespace) + ), + 'p\.tenant_id', + 'g' +); + +-- Contar ocurrencias de p.user_id (debe aparecer en los 3 INSERTs) +\echo '' +\echo ' 4.4 Ocurrencias de "p.user_id":' +SELECT COUNT(*)::text || ' ocurrencias (esperado: 3)' as resultado +FROM regexp_matches( + pg_get_functiondef( + (SELECT oid FROM pg_proc WHERE proname = 'generate_student_alerts' AND pronamespace = 'progress_tracking'::regnamespace) + ), + 'p\.user_id', + 'g' +); + +-- ===================================================== +-- 5. PRUEBA FUNCIONAL (OPCIONAL) +-- ===================================================== +\echo '' +\echo '5. Prueba funcional (opcional)...' +\echo ' Si desea ejecutar la función, ejecute:' +\echo ' SELECT progress_tracking.generate_student_alerts();' +\echo '' + +-- ===================================================== +-- 6. ANÁLISIS DE DATOS DE PRUEBA (SI EXISTEN) +-- ===================================================== +\echo '6. Analizando datos existentes...' +\echo '' + +\echo ' 6.1 Estudiantes con progreso en módulos:' +SELECT COUNT(DISTINCT user_id) as total_students +FROM progress_tracking.module_progress; + +\echo '' +\echo ' 6.2 Estudiantes con ejercicios intentados:' +SELECT COUNT(DISTINCT user_id) as total_students +FROM progress_tracking.exercise_submissions; + +\echo '' +\echo ' 6.3 Verificar que profiles.user_id mapea a auth.users.id:' +SELECT + COUNT(*) as total_profiles, + COUNT(DISTINCT p.id) as unique_profile_ids, + COUNT(DISTINCT p.user_id) as unique_user_ids, + COUNT(DISTINCT u.id) as unique_auth_user_ids, + CASE + WHEN COUNT(DISTINCT p.user_id) = COUNT(DISTINCT u.id) THEN 'CORRECTO ✓' + ELSE 'INCONSISTENTE ✗' + END as mapping_status +FROM auth_management.profiles p +LEFT JOIN auth.users u ON p.user_id = u.id; + +\echo '' +\echo ' 6.4 Alertas generadas actualmente:' +SELECT + alert_type, + COUNT(*) as total, + COUNT(DISTINCT student_id) as unique_students +FROM progress_tracking.student_intervention_alerts +GROUP BY alert_type +ORDER BY alert_type; + +-- ===================================================== +-- 7. RESUMEN DE VALIDACIÓN +-- ===================================================== +\echo '' +\echo '=========================================' +\echo 'RESUMEN DE VALIDACIÓN' +\echo '=========================================' +\echo '' +\echo 'CRITERIOS DE ACEPTACIÓN:' +\echo ' ✓ Función usa JOIN auth_management.profiles (3 veces)' +\echo ' ✓ Función NO usa JOIN auth.users (0 veces)' +\echo ' ✓ Función usa p.tenant_id (3 veces)' +\echo ' ✓ Función usa p.user_id (3 veces)' +\echo ' ✓ FKs verificadas:' +\echo ' - module_progress.user_id → profiles(id)' +\echo ' - exercise_submissions.user_id → profiles(id)' +\echo ' - student_intervention_alerts.student_id → auth.users(id)' +\echo ' - profiles.user_id → auth.users(id)' +\echo '' +\echo 'Si todos los criterios se cumplen: ✅ VALIDACIÓN EXITOSA' +\echo 'Si algún criterio falla: ✗ REVISAR IMPLEMENTACIÓN' +\echo '' +\echo '=========================================' diff --git a/projects/gamilit/apps/database/scripts/validations/validate-missions-objectives-structure.sql b/projects/gamilit/apps/database/scripts/validations/validate-missions-objectives-structure.sql new file mode 100644 index 0000000..5c4f692 --- /dev/null +++ b/projects/gamilit/apps/database/scripts/validations/validate-missions-objectives-structure.sql @@ -0,0 +1,135 @@ +-- ===================================================== +-- Script de Validación: Estructura de Objectives en Missions +-- Fecha: 2025-11-26 +-- Propósito: Validar que initialize_user_missions crea objectives como ARRAY +-- ===================================================== + +\echo '==================================================' +\echo 'VALIDACIÓN: Estructura de objectives en missions' +\echo '==================================================' +\echo '' + +DO $$ +DECLARE + v_test_user_id UUID; + v_objectives JSONB; + v_objectives_type TEXT; + v_mission_count INT; + v_total_missions INT; + v_test_passed BOOLEAN := true; +BEGIN + RAISE NOTICE '1. Buscando usuario de prueba...'; + + -- Buscar un usuario existente + SELECT id INTO v_test_user_id + FROM auth_management.profiles + LIMIT 1; + + IF v_test_user_id IS NULL THEN + RAISE EXCEPTION 'No hay usuarios en la base de datos para testing'; + END IF; + + RAISE NOTICE 'Usuario de prueba: %', v_test_user_id; + + RAISE NOTICE '2. Limpiando misiones previas...'; + DELETE FROM gamification_system.missions WHERE user_id = v_test_user_id; + + RAISE NOTICE '3. Ejecutando initialize_user_missions()...'; + PERFORM gamilit.initialize_user_missions(v_test_user_id); + + RAISE NOTICE '4. Verificando estructura de objectives...'; + + -- Verificar que se crearon 8 misiones + SELECT COUNT(*) INTO v_total_missions + FROM gamification_system.missions + WHERE user_id = v_test_user_id; + + RAISE NOTICE 'Misiones creadas: %', v_total_missions; + + IF v_total_missions != 8 THEN + RAISE NOTICE '❌ ERROR: Se esperaban 8 misiones, se crearon %', v_total_missions; + v_test_passed := false; + ELSE + RAISE NOTICE '✅ OK: Se crearon las 8 misiones esperadas'; + END IF; + + -- Verificar que TODAS las misiones tienen objectives como ARRAY + SELECT objectives, jsonb_typeof(objectives) + INTO v_objectives, v_objectives_type + FROM gamification_system.missions + WHERE user_id = v_test_user_id + LIMIT 1; + + RAISE NOTICE 'Tipo de objectives: %', v_objectives_type; + RAISE NOTICE 'Ejemplo de objectives: %', v_objectives; + + IF v_objectives_type != 'array' THEN + RAISE NOTICE '❌ ERROR: objectives NO es un array, es: %', v_objectives_type; + v_test_passed := false; + ELSE + RAISE NOTICE '✅ OK: objectives es un ARRAY'; + END IF; + + RAISE NOTICE '5. Verificando compatibilidad con operador @>...'; + + -- Test: Buscar misiones de tipo complete_exercises + SELECT COUNT(*) INTO v_mission_count + FROM gamification_system.missions + WHERE user_id = v_test_user_id + AND objectives @> '[{"type": "complete_exercises"}]'::jsonb; + + RAISE NOTICE 'Misiones con type=complete_exercises encontradas con @>: %', v_mission_count; + + IF v_mission_count = 0 THEN + RAISE NOTICE '❌ ERROR: El operador @> NO encuentra misiones'; + v_test_passed := false; + ELSE + RAISE NOTICE '✅ OK: El operador @> funciona correctamente'; + END IF; + + -- Test: Buscar misión de earn_xp + SELECT COUNT(*) INTO v_mission_count + FROM gamification_system.missions + WHERE user_id = v_test_user_id + AND objectives @> '[{"type": "earn_xp"}]'::jsonb; + + RAISE NOTICE 'Misiones con type=earn_xp encontradas: %', v_mission_count; + + IF v_mission_count = 0 THEN + RAISE NOTICE '❌ ERROR: No se encuentra misión de earn_xp'; + v_test_passed := false; + END IF; + + -- Test: Verificar misión weekly_explorer con modules_visited + SELECT COUNT(*) INTO v_mission_count + FROM gamification_system.missions + WHERE user_id = v_test_user_id + AND template_id = 'weekly_explorer' + AND objectives @> '[{"type": "explore_modules"}]'::jsonb + AND objectives::text LIKE '%modules_visited%'; + + RAISE NOTICE 'Misión weekly_explorer con modules_visited: %', v_mission_count; + + IF v_mission_count = 0 THEN + RAISE NOTICE '❌ ERROR: weekly_explorer no tiene modules_visited'; + v_test_passed := false; + ELSE + RAISE NOTICE '✅ OK: weekly_explorer tiene modules_visited'; + END IF; + + RAISE NOTICE '6. Limpiando datos de prueba...'; + DELETE FROM gamification_system.missions WHERE user_id = v_test_user_id; + + RAISE NOTICE ''; + RAISE NOTICE '=================================================='; + IF v_test_passed THEN + RAISE NOTICE '✅ ✅ ✅ VALIDACIÓN EXITOSA ✅ ✅ ✅'; + RAISE NOTICE 'La función initialize_user_missions está correcta'; + ELSE + RAISE NOTICE '❌ ❌ ❌ VALIDACIÓN FALLIDA ❌ ❌ ❌'; + RAISE NOTICE 'Hay problemas en la función initialize_user_missions'; + END IF; + RAISE NOTICE '=================================================='; + +END; +$$; diff --git a/projects/gamilit/apps/database/scripts/validations/validate-seeds-integrity.sql b/projects/gamilit/apps/database/scripts/validations/validate-seeds-integrity.sql new file mode 100644 index 0000000..05816df --- /dev/null +++ b/projects/gamilit/apps/database/scripts/validations/validate-seeds-integrity.sql @@ -0,0 +1,248 @@ +-- ===================================================== +-- Script: Validate Seeds Integrity +-- Description: Valida integridad referencial de todos los seeds +-- Created: 2025-11-15 +-- Version: 1.0 +-- ===================================================== +-- +-- PROPÓSITO: +-- Este script verifica que: +-- 1. No haya registros huérfanos (FK rotas) +-- 2. Conteos de registros coincidan (users = profiles = user_stats) +-- 3. Triggers funcionaron correctamente +-- 4. Seeds sociales tienen datos suficientes +-- +-- EJECUCIÓN: +-- psql -U gamilit_user -d gamilit_platform -f validate-seeds-integrity.sql +-- ===================================================== + +\set QUIET on +\timing off + +-- Configurar search path +SET search_path TO auth_management, gamification_system, social_features, educational_content, public; + +-- ===================================================== +-- Sección 1: Conteos Básicos +-- ===================================================== + +\echo '' +\echo '========================================' +\echo '1. CONTEOS BÁSICOS' +\echo '========================================' + +DO $$ +DECLARE + users_count INTEGER; + profiles_count INTEGER; + user_stats_count INTEGER; + user_ranks_count INTEGER; + comodines_count INTEGER; +BEGIN + SELECT COUNT(*) INTO users_count FROM auth.users; + SELECT COUNT(*) INTO profiles_count FROM auth_management.profiles; + SELECT COUNT(*) INTO user_stats_count FROM gamification_system.user_stats; + SELECT COUNT(*) INTO user_ranks_count FROM gamification_system.user_ranks; + SELECT COUNT(*) INTO comodines_count FROM gamification_system.comodines_inventory; + + RAISE NOTICE 'auth.users: %', users_count; + RAISE NOTICE 'auth_management.profiles: %', profiles_count; + RAISE NOTICE 'gamification_system.user_stats: %', user_stats_count; + RAISE NOTICE 'gamification_system.user_ranks: %', user_ranks_count; + RAISE NOTICE 'gamification_system.comodines_inventory: %', comodines_count; + RAISE NOTICE ''; + + IF users_count = profiles_count AND profiles_count = user_stats_count AND user_stats_count = user_ranks_count THEN + RAISE NOTICE '✓ PASS: Todos los conteos coinciden (%)', users_count; + ELSE + RAISE WARNING '✗ FAIL: Conteos no coinciden'; + RAISE WARNING 'Diferencias detectadas - verificar triggers y seeds'; + END IF; +END $$; + +-- ===================================================== +-- Sección 2: Integridad Referencial +-- ===================================================== + +\echo '' +\echo '========================================' +\echo '2. INTEGRIDAD REFERENCIAL' +\echo '========================================' + +DO $$ +DECLARE + orphan_profiles INTEGER; + orphan_user_stats INTEGER; + orphan_user_ranks INTEGER; + orphan_comodines INTEGER; +BEGIN + -- Profiles sin user + SELECT COUNT(*) INTO orphan_profiles + FROM auth_management.profiles p + LEFT JOIN auth.users u ON u.id = p.user_id + WHERE u.id IS NULL; + + -- User_stats sin profile + SELECT COUNT(*) INTO orphan_user_stats + FROM gamification_system.user_stats us + LEFT JOIN auth_management.profiles p ON p.user_id = us.user_id + WHERE p.id IS NULL; + + -- User_ranks sin user_stats + SELECT COUNT(*) INTO orphan_user_ranks + FROM gamification_system.user_ranks ur + LEFT JOIN gamification_system.user_stats us ON us.user_id = ur.user_id + WHERE us.id IS NULL; + + -- Comodines sin user + SELECT COUNT(*) INTO orphan_comodines + FROM gamification_system.comodines_inventory ci + LEFT JOIN auth_management.profiles p ON p.user_id = ci.user_id + WHERE p.id IS NULL; + + RAISE NOTICE 'Profiles huérfanos (sin user): %', orphan_profiles; + RAISE NOTICE 'User_stats huérfanos (sin profile): %', orphan_user_stats; + RAISE NOTICE 'User_ranks huérfanos (sin user_stats): %', orphan_user_ranks; + RAISE NOTICE 'Comodines huérfanos (sin user): %', orphan_comodines; + RAISE NOTICE ''; + + IF orphan_profiles = 0 AND orphan_user_stats = 0 AND orphan_user_ranks = 0 AND orphan_comodines = 0 THEN + RAISE NOTICE '✓ PASS: No hay registros huérfanos'; + ELSE + RAISE WARNING '✗ FAIL: Se encontraron registros huérfanos'; + RAISE WARNING 'Ejecutar limpieza de huérfanos'; + END IF; +END $$; + +-- ===================================================== +-- Sección 3: Datos Educativos +-- ===================================================== + +\echo '' +\echo '========================================' +\echo '3. CONTENIDO EDUCATIVO' +\echo '========================================' + +DO $$ +DECLARE + modules_count INTEGER; + published_modules INTEGER; + exercises_count INTEGER; + achievements_count INTEGER; + ranks_count INTEGER; +BEGIN + SELECT COUNT(*) INTO modules_count FROM educational_content.modules; + SELECT COUNT(*) INTO published_modules FROM educational_content.modules WHERE is_published = true; + SELECT COUNT(*) INTO exercises_count FROM educational_content.exercises; + SELECT COUNT(*) INTO achievements_count FROM gamification_system.achievements WHERE is_active = true; + SELECT COUNT(*) INTO ranks_count FROM gamification_system.maya_ranks WHERE is_active = true; + + RAISE NOTICE 'Módulos: % (% publicados)', modules_count, published_modules; + RAISE NOTICE 'Ejercicios: %', exercises_count; + RAISE NOTICE 'Achievements: %', achievements_count; + RAISE NOTICE 'Rangos Maya: %', ranks_count; + RAISE NOTICE ''; + + IF modules_count >= 5 AND exercises_count >= 50 AND achievements_count >= 15 AND ranks_count >= 5 THEN + RAISE NOTICE '✓ PASS: Contenido educativo completo'; + ELSE + RAISE WARNING '✗ FAIL: Contenido educativo incompleto'; + END IF; +END $$; + +-- ===================================================== +-- Sección 4: Features Sociales +-- ===================================================== + +\echo '' +\echo '========================================' +\echo '4. FEATURES SOCIALES' +\echo '========================================' + +DO $$ +DECLARE + friendships_count INTEGER; + pending_requests INTEGER; + schools_count INTEGER; + classrooms_count INTEGER; +BEGIN + SELECT COUNT(*) INTO friendships_count FROM social_features.friendships WHERE status = 'accepted'; + SELECT COUNT(*) INTO pending_requests FROM social_features.friendships WHERE status = 'pending'; + SELECT COUNT(*) INTO schools_count FROM social_features.schools; + SELECT COUNT(*) INTO classrooms_count FROM social_features.classrooms; + + RAISE NOTICE 'Friendships aceptados: %', friendships_count; + RAISE NOTICE 'Friend requests pendientes: %', pending_requests; + RAISE NOTICE 'Escuelas: %', schools_count; + RAISE NOTICE 'Aulas: %', classrooms_count; + RAISE NOTICE ''; + + IF friendships_count >= 8 AND schools_count >= 2 THEN + RAISE NOTICE '✓ PASS: Features sociales disponibles'; + ELSE + RAISE WARNING '✗ FAIL: Features sociales incompletas'; + END IF; +END $$; + +-- ===================================================== +-- Sección 5: Resumen Final +-- ===================================================== + +\echo '' +\echo '========================================' +\echo 'RESUMEN FINAL' +\echo '========================================' + +DO $$ +DECLARE + total_users INTEGER; + total_profiles INTEGER; + total_stats INTEGER; + avg_level NUMERIC; + total_coins INTEGER; + total_achievements INTEGER; + total_modules INTEGER; + total_friendships INTEGER; +BEGIN + SELECT COUNT(*) INTO total_users FROM auth.users; + SELECT COUNT(*) INTO total_profiles FROM auth_management.profiles; + SELECT COUNT(*) INTO total_stats FROM gamification_system.user_stats; + SELECT COUNT(*) INTO total_achievements FROM gamification_system.achievements; + SELECT COUNT(*) INTO total_modules FROM educational_content.modules; + SELECT COUNT(*) INTO total_friendships FROM social_features.friendships WHERE status = 'accepted'; + + SELECT AVG(level)::NUMERIC(5,2) INTO avg_level FROM gamification_system.user_stats; + SELECT SUM(ml_coins) INTO total_coins FROM gamification_system.user_stats; + + RAISE NOTICE 'Base de Datos: gamilit_platform'; + RAISE NOTICE 'Fecha validación: %', now(); + RAISE NOTICE ''; + RAISE NOTICE 'Usuarios totales: %', total_users; + RAISE NOTICE 'Perfiles completos: %', total_profiles; + RAISE NOTICE 'User stats: %', total_stats; + RAISE NOTICE ''; + RAISE NOTICE 'Nivel promedio usuarios: %', avg_level; + RAISE NOTICE 'ML Coins en circulación: %', total_coins; + RAISE NOTICE 'Achievements disponibles: %', total_achievements; + RAISE NOTICE 'Módulos educativos: %', total_modules; + RAISE NOTICE 'Amistades activas: %', total_friendships; + RAISE NOTICE ''; + + IF total_users = total_profiles AND total_profiles = total_stats THEN + RAISE NOTICE '════════════════════════════════════════'; + RAISE NOTICE '✓✓✓ VALIDACIÓN COMPLETA: SUCCESS ✓✓✓'; + RAISE NOTICE '════════════════════════════════════════'; + RAISE NOTICE 'Seeds están correctos y listos para desarrollo frontend'; + ELSE + RAISE WARNING '════════════════════════════════════════'; + RAISE WARNING '✗✗✗ VALIDACIÓN: PROBLEMAS DETECTADOS ✗✗✗'; + RAISE WARNING '════════════════════════════════════════'; + RAISE WARNING 'Revisar secciones anteriores para detalles'; + END IF; + + RAISE NOTICE ''; +END $$; + +\echo '' +\echo 'Validación completada.' +\echo '' diff --git a/projects/gamilit/apps/database/scripts/validations/validate-update-user-rank-fix.sql b/projects/gamilit/apps/database/scripts/validations/validate-update-user-rank-fix.sql new file mode 100644 index 0000000..9fa0799 --- /dev/null +++ b/projects/gamilit/apps/database/scripts/validations/validate-update-user-rank-fix.sql @@ -0,0 +1,231 @@ +-- ===================================================================================== +-- Script: Validación de corrección update_user_rank() +-- Propósito: Validar que la función update_user_rank() incluye balance_before y balance_after +-- Fecha: 2025-11-24 +-- Uso: psql -d gamilit_platform -f scripts/validate-update-user-rank-fix.sql +-- ===================================================================================== + +\echo '' +\echo '=========================================' +\echo 'VALIDACIÓN: update_user_rank() - Balance Fields' +\echo '=========================================' +\echo '' + +-- ===================================================================================== +-- PASO 1: Verificar existencia de la función +-- ===================================================================================== +\echo '1. Verificando existencia de función...' +SELECT + p.proname AS function_name, + n.nspname AS schema_name, + pg_get_function_result(p.oid) AS return_type, + pg_get_function_arguments(p.oid) AS arguments +FROM pg_proc p +JOIN pg_namespace n ON p.pronamespace = n.oid +WHERE p.proname = 'update_user_rank' + AND n.nspname = 'gamification_system'; + +\echo '' + +-- ===================================================================================== +-- PASO 2: Verificar estructura de la tabla ml_coins_transactions +-- ===================================================================================== +\echo '2. Verificando estructura de ml_coins_transactions...' +SELECT + column_name, + data_type, + is_nullable, + column_default +FROM information_schema.columns +WHERE table_schema = 'gamification_system' + AND table_name = 'ml_coins_transactions' + AND column_name IN ('balance_before', 'balance_after', 'transaction_type', 'amount', 'user_id') +ORDER BY ordinal_position; + +\echo '' + +-- ===================================================================================== +-- PASO 3: Verificar valores del ENUM transaction_type +-- ===================================================================================== +\echo '3. Verificando ENUM transaction_type...' +SELECT + t.typname AS enum_name, + e.enumlabel AS enum_value, + e.enumsortorder AS sort_order +FROM pg_type t +JOIN pg_enum e ON t.oid = e.enumtypid +JOIN pg_namespace n ON t.typnamespace = n.oid +WHERE t.typname = 'transaction_type' + AND n.nspname = 'gamification_system' +ORDER BY e.enumsortorder; + +\echo '' + +-- ===================================================================================== +-- PASO 4: Verificar que 'earned_rank' existe en el ENUM +-- ===================================================================================== +\echo '4. Verificando que ''earned_rank'' existe en el ENUM...' +SELECT + CASE + WHEN EXISTS ( + SELECT 1 + FROM pg_type t + JOIN pg_enum e ON t.oid = e.enumtypid + JOIN pg_namespace n ON t.typnamespace = n.oid + WHERE t.typname = 'transaction_type' + AND n.nspname = 'gamification_system' + AND e.enumlabel = 'earned_rank' + ) THEN '✅ ENUM ''earned_rank'' existe' + ELSE '❌ ERROR: ENUM ''earned_rank'' NO existe' + END AS validation_result; + +\echo '' + +-- ===================================================================================== +-- PASO 5: Ver código fuente de la función (últimas líneas del INSERT) +-- ===================================================================================== +\echo '5. Verificando código fuente de la función (fragmento del INSERT)...' +SELECT + substring( + pg_get_functiondef(p.oid), + position('INSERT INTO gamification_system.ml_coins_transactions' in pg_get_functiondef(p.oid)), + 500 + ) AS insert_statement_fragment +FROM pg_proc p +JOIN pg_namespace n ON p.pronamespace = n.oid +WHERE p.proname = 'update_user_rank' + AND n.nspname = 'gamification_system'; + +\echo '' + +-- ===================================================================================== +-- PASO 6: Test básico de sintaxis (sin ejecutar realmente) +-- ===================================================================================== +\echo '6. Validación de sintaxis SQL...' +\echo 'Preparando transacción de prueba (se hará ROLLBACK)...' + +BEGIN; + +-- Crear usuario de prueba temporal +DO $$ +DECLARE + v_test_user_id UUID := 'test-user-validate-rank-fix'::UUID; +BEGIN + -- Limpiar si existe + DELETE FROM gamification_system.user_stats WHERE user_id = v_test_user_id; + DELETE FROM gamification_system.user_ranks WHERE user_id = v_test_user_id; + DELETE FROM auth_management.profiles WHERE id = v_test_user_id; + + -- Crear perfil de prueba + INSERT INTO auth_management.profiles (id, display_name, role, tenant_id) + VALUES ( + v_test_user_id, + 'Test User - Validate Rank Fix', + 'student', + (SELECT id FROM auth_management.tenants LIMIT 1) + ); + + -- Crear user_stats inicial (debe disparar trigger de inicialización) + -- Si el trigger funciona, creará el registro automáticamente + + RAISE NOTICE 'Usuario de prueba creado: %', v_test_user_id; +END $$; + +-- Verificar que el usuario se creó correctamente +\echo '' +\echo 'Verificando creación de user_stats...' +SELECT + user_id, + total_xp, + COALESCE(ml_coins, 0) AS ml_coins, + created_at +FROM gamification_system.user_stats +WHERE user_id = 'test-user-validate-rank-fix'::UUID; + +-- Simular XP suficiente para ascender de rango +UPDATE gamification_system.user_stats +SET total_xp = 5000 -- Suficiente para pasar de Ajaw +WHERE user_id = 'test-user-validate-rank-fix'::UUID; + +\echo '' +\echo 'Ejecutando update_user_rank()...' +SELECT * FROM gamification_system.update_user_rank('test-user-validate-rank-fix'::UUID); + +\echo '' +\echo 'Verificando transacción creada...' +SELECT + user_id, + amount, + balance_before, + balance_after, + transaction_type, + description, + created_at +FROM gamification_system.ml_coins_transactions +WHERE user_id = 'test-user-validate-rank-fix'::UUID + AND transaction_type = 'earned_rank' +ORDER BY created_at DESC +LIMIT 1; + +\echo '' +\echo 'Validando integridad de balance...' +SELECT + CASE + WHEN balance_after = balance_before + amount THEN '✅ Balance correcto' + ELSE '❌ ERROR: Balance incorrecto' + END AS balance_validation, + balance_before, + amount, + balance_after, + (balance_before + amount) AS expected_balance_after +FROM gamification_system.ml_coins_transactions +WHERE user_id = 'test-user-validate-rank-fix'::UUID + AND transaction_type = 'earned_rank' +ORDER BY created_at DESC +LIMIT 1; + +-- Rollback para no afectar la base de datos +ROLLBACK; + +\echo '' +\echo '✅ Transacción de prueba revertida (ROLLBACK)' +\echo '' + +-- ===================================================================================== +-- PASO 7: Resumen de validación +-- ===================================================================================== +\echo '=========================================' +\echo 'RESUMEN DE VALIDACIÓN' +\echo '=========================================' +\echo '' +\echo 'Checklist de corrección:' +\echo ' [ ] Función update_user_rank() existe' +\echo ' [ ] Campos balance_before y balance_after existen en ml_coins_transactions' +\echo ' [ ] Campos son NOT NULL' +\echo ' [ ] ENUM transaction_type tiene valor ''earned_rank''' +\echo ' [ ] Función incluye balance_before y balance_after en INSERT' +\echo ' [ ] Balance calculado correctamente (balance_after = balance_before + amount)' +\echo '' +\echo 'Si todos los pasos anteriores mostraron ✅, la corrección es exitosa.' +\echo '' + +-- ===================================================================================== +-- PASO 8: Instrucciones finales +-- ===================================================================================== +\echo '=========================================' +\echo 'INSTRUCCIONES' +\echo '=========================================' +\echo '' +\echo 'Para aplicar la corrección a producción:' +\echo ' 1. Verificar que este script ejecuta sin errores' +\echo ' 2. Aplicar función: psql -d gamilit_platform -f ddl/schemas/gamification_system/functions/update_user_rank.sql' +\echo ' 3. Validar con usuarios reales en ambiente de staging' +\echo ' 4. Deploy a producción' +\echo '' +\echo 'Para revisar otras funciones que usan ml_coins_transactions:' +\echo ' grep -r "INSERT INTO.*ml_coins_transactions" apps/database/ddl/' +\echo '' + +-- ===================================================================================== +-- FIN DEL SCRIPT +-- ===================================================================================== diff --git a/projects/gamilit/apps/database/scripts/validations/validate-user-initialization.sql b/projects/gamilit/apps/database/scripts/validations/validate-user-initialization.sql new file mode 100644 index 0000000..7b8e7d9 --- /dev/null +++ b/projects/gamilit/apps/database/scripts/validations/validate-user-initialization.sql @@ -0,0 +1,499 @@ +-- ===================================================== +-- Script: validate-user-initialization.sql +-- Description: Valida que TODOS los usuarios estén completamente inicializados +-- Version: 1.0 +-- Created: 2025-11-24 +-- Database-Agent Task: Análisis y corrección de inicialización de usuarios +-- ===================================================== +-- +-- OBJETIVO: +-- Validar que todos los usuarios (testing + demo + producción) tengan: +-- 1. auth.users (registro inicial) +-- 2. auth_management.profiles (con profiles.id = auth.users.id) +-- 3. gamification_system.user_stats (ML Coins inicializados) +-- 4. gamification_system.comodines_inventory (user_id → profiles.id) +-- 5. gamification_system.user_ranks (rango inicial Ajaw) +-- 6. progress_tracking.module_progress (todos los módulos publicados) +-- +-- USUARIOS ESPERADOS: +-- - Testing PROD (@gamilit.com): 3 usuarios +-- - Demo PROD (@demo.glit.edu.mx): 20 usuarios (opcional según ambiente) +-- - Producción (emails reales): 13 usuarios +-- - TOTAL PROD: 16 usuarios (3 testing + 13 producción) +-- - TOTAL FULL: 36 usuarios (3 testing + 20 demo + 13 producción) +-- ===================================================== + +\set ON_ERROR_STOP off + +SET search_path TO auth, auth_management, gamification_system, progress_tracking, public; + +-- ===================================================== +-- SECCIÓN 1: Validación de auth.users +-- ===================================================== + +\echo '========================================' +\echo 'VALIDACIÓN 1: auth.users' +\echo '========================================' + +SELECT + '1.1. Total usuarios en auth.users' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) >= 16 THEN '✅ OK (mínimo 16 esperados)' + ELSE '❌ ERROR: Menos de 16 usuarios' + END AS resultado +FROM auth.users; + +SELECT + '1.2. Usuarios @gamilit.com (testing)' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) = 3 THEN '✅ OK (3 esperados)' + ELSE '❌ ERROR: Se esperaban 3 usuarios @gamilit.com' + END AS resultado +FROM auth.users +WHERE email LIKE '%@gamilit.com'; + +SELECT + '1.3. Usuarios productivos (no @gamilit.com)' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) = 13 THEN '✅ OK (13 esperados)' + ELSE '⚠️ WARNING: Se esperaban 13 usuarios productivos' + END AS resultado +FROM auth.users +WHERE email NOT LIKE '%@gamilit.com' + AND email NOT LIKE '%@demo.glit.edu.mx'; + +SELECT + '1.4. Usuarios DEMO (@demo.glit.edu.mx)' AS validacion, + COUNT(*) AS cantidad, + '⏭️ OPCIONAL (ambiente)' AS resultado +FROM auth.users +WHERE email LIKE '%@demo.glit.edu.mx'; + +-- ===================================================== +-- SECCIÓN 2: Validación de auth_management.profiles +-- ===================================================== + +\echo '' +\echo '========================================' +\echo 'VALIDACIÓN 2: auth_management.profiles' +\echo '========================================' + +SELECT + '2.1. Total profiles' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) >= 16 THEN '✅ OK (mínimo 16 esperados)' + ELSE '❌ ERROR: Menos de 16 profiles' + END AS resultado +FROM auth_management.profiles; + +SELECT + '2.2. Profiles con id = user_id (CRÍTICO)' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) = (SELECT COUNT(*) FROM auth_management.profiles) + THEN '✅ OK (100% consistente)' + ELSE '❌ ERROR: Hay profiles con id ≠ user_id' + END AS resultado +FROM auth_management.profiles +WHERE id = user_id; + +SELECT + '2.3. Usuarios SIN profile (CRÍTICO)' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) = 0 THEN '✅ OK (todos tienen profile)' + ELSE '❌ ERROR: Hay usuarios sin profile' + END AS resultado +FROM auth.users u +LEFT JOIN auth_management.profiles p ON u.id = p.user_id +WHERE p.id IS NULL; + +-- Mostrar usuarios sin profile (si existen) +DO $$ +DECLARE + usuarios_sin_profile INTEGER; +BEGIN + SELECT COUNT(*) INTO usuarios_sin_profile + FROM auth.users u + LEFT JOIN auth_management.profiles p ON u.id = p.user_id + WHERE p.id IS NULL; + + IF usuarios_sin_profile > 0 THEN + RAISE NOTICE ''; + RAISE NOTICE '❌ USUARIOS SIN PROFILE DETECTADOS:'; + FOR rec IN + SELECT u.id, u.email + FROM auth.users u + LEFT JOIN auth_management.profiles p ON u.id = p.user_id + WHERE p.id IS NULL + LOOP + RAISE NOTICE ' - % (%)', rec.email, rec.id; + END LOOP; + END IF; +END $$; + +-- ===================================================== +-- SECCIÓN 3: Validación de gamification_system.user_stats +-- ===================================================== + +\echo '' +\echo '========================================' +\echo 'VALIDACIÓN 3: gamification_system.user_stats' +\echo '========================================' + +SELECT + '3.1. Total user_stats' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) >= 16 THEN '✅ OK (mínimo 16 esperados)' + ELSE '❌ ERROR: Menos de 16 user_stats' + END AS resultado +FROM gamification_system.user_stats; + +SELECT + '3.2. Usuarios CON profile pero SIN user_stats (CRÍTICO)' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) = 0 THEN '✅ OK (todos tienen user_stats)' + ELSE '❌ ERROR: Hay profiles sin user_stats' + END AS resultado +FROM auth_management.profiles p +LEFT JOIN gamification_system.user_stats us ON p.user_id = us.user_id +WHERE us.user_id IS NULL + AND p.role IN ('student', 'admin_teacher', 'super_admin'); + +SELECT + '3.3. user_stats con ML Coins = 100 (inicial)' AS validacion, + COUNT(*) AS cantidad, + '⏭️ INFO (bonus inicial)' AS resultado +FROM gamification_system.user_stats +WHERE ml_coins = 100 AND ml_coins_earned_total = 100; + +-- Mostrar profiles sin user_stats (si existen) +DO $$ +DECLARE + profiles_sin_stats INTEGER; +BEGIN + SELECT COUNT(*) INTO profiles_sin_stats + FROM auth_management.profiles p + LEFT JOIN gamification_system.user_stats us ON p.user_id = us.user_id + WHERE us.user_id IS NULL + AND p.role IN ('student', 'admin_teacher', 'super_admin'); + + IF profiles_sin_stats > 0 THEN + RAISE NOTICE ''; + RAISE NOTICE '❌ PROFILES SIN USER_STATS DETECTADOS:'; + FOR rec IN + SELECT p.id, p.email, p.role + FROM auth_management.profiles p + LEFT JOIN gamification_system.user_stats us ON p.user_id = us.user_id + WHERE us.user_id IS NULL + AND p.role IN ('student', 'admin_teacher', 'super_admin') + LOOP + RAISE NOTICE ' - % (%, %)', rec.email, rec.role, rec.id; + END LOOP; + END IF; +END $$; + +-- ===================================================== +-- SECCIÓN 4: Validación de gamification_system.comodines_inventory +-- ===================================================== + +\echo '' +\echo '========================================' +\echo 'VALIDACIÓN 4: gamification_system.comodines_inventory' +\echo '========================================' + +SELECT + '4.1. Total comodines_inventory' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) >= 16 THEN '✅ OK (mínimo 16 esperados)' + ELSE '❌ ERROR: Menos de 16 inventarios' + END AS resultado +FROM gamification_system.comodines_inventory; + +SELECT + '4.2. Profiles SIN comodines_inventory (CRÍTICO)' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) = 0 THEN '✅ OK (todos tienen inventario)' + ELSE '❌ ERROR: Hay profiles sin inventario' + END AS resultado +FROM auth_management.profiles p +LEFT JOIN gamification_system.comodines_inventory ci ON p.id = ci.user_id +WHERE ci.user_id IS NULL + AND p.role IN ('student', 'admin_teacher', 'super_admin'); + +-- IMPORTANTE: comodines_inventory.user_id apunta a profiles.id (NO auth.users.id) +SELECT + '4.3. comodines_inventory con user_id válido' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) = (SELECT COUNT(*) FROM gamification_system.comodines_inventory) + THEN '✅ OK (100% válidos)' + ELSE '❌ ERROR: Hay inventarios con user_id inválido' + END AS resultado +FROM gamification_system.comodines_inventory ci +INNER JOIN auth_management.profiles p ON ci.user_id = p.id; + +-- Mostrar profiles sin comodines_inventory (si existen) +DO $$ +DECLARE + profiles_sin_inventory INTEGER; +BEGIN + SELECT COUNT(*) INTO profiles_sin_inventory + FROM auth_management.profiles p + LEFT JOIN gamification_system.comodines_inventory ci ON p.id = ci.user_id + WHERE ci.user_id IS NULL + AND p.role IN ('student', 'admin_teacher', 'super_admin'); + + IF profiles_sin_inventory > 0 THEN + RAISE NOTICE ''; + RAISE NOTICE '❌ PROFILES SIN COMODINES_INVENTORY DETECTADOS:'; + FOR rec IN + SELECT p.id, p.email, p.role + FROM auth_management.profiles p + LEFT JOIN gamification_system.comodines_inventory ci ON p.id = ci.user_id + WHERE ci.user_id IS NULL + AND p.role IN ('student', 'admin_teacher', 'super_admin') + LOOP + RAISE NOTICE ' - % (%, %)', rec.email, rec.role, rec.id; + END LOOP; + END IF; +END $$; + +-- ===================================================== +-- SECCIÓN 5: Validación de gamification_system.user_ranks +-- ===================================================== + +\echo '' +\echo '========================================' +\echo 'VALIDACIÓN 5: gamification_system.user_ranks' +\echo '========================================' + +SELECT + '5.1. Total user_ranks' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) >= 16 THEN '✅ OK (mínimo 16 esperados)' + ELSE '❌ ERROR: Menos de 16 user_ranks' + END AS resultado +FROM gamification_system.user_ranks; + +SELECT + '5.2. Usuarios CON profile pero SIN user_ranks (CRÍTICO)' AS validacion, + COUNT(*) AS cantidad, + CASE + WHEN COUNT(*) = 0 THEN '✅ OK (todos tienen rank)' + ELSE '❌ ERROR: Hay profiles sin rank' + END AS resultado +FROM auth_management.profiles p +LEFT JOIN gamification_system.user_ranks ur ON p.user_id = ur.user_id +WHERE ur.user_id IS NULL + AND p.role IN ('student', 'admin_teacher', 'super_admin'); + +SELECT + '5.3. user_ranks con rango Ajaw (inicial)' AS validacion, + COUNT(*) AS cantidad, + '⏭️ INFO (rango inicial)' AS resultado +FROM gamification_system.user_ranks +WHERE current_rank = 'Ajaw'::gamification_system.maya_rank; + +-- Mostrar profiles sin user_ranks (si existen) +DO $$ +DECLARE + profiles_sin_ranks INTEGER; +BEGIN + SELECT COUNT(*) INTO profiles_sin_ranks + FROM auth_management.profiles p + LEFT JOIN gamification_system.user_ranks ur ON p.user_id = ur.user_id + WHERE ur.user_id IS NULL + AND p.role IN ('student', 'admin_teacher', 'super_admin'); + + IF profiles_sin_ranks > 0 THEN + RAISE NOTICE ''; + RAISE NOTICE '❌ PROFILES SIN USER_RANKS DETECTADOS:'; + FOR rec IN + SELECT p.id, p.email, p.role + FROM auth_management.profiles p + LEFT JOIN gamification_system.user_ranks ur ON p.user_id = ur.user_id + WHERE ur.user_id IS NULL + AND p.role IN ('student', 'admin_teacher', 'super_admin') + LOOP + RAISE NOTICE ' - % (%, %)', rec.email, rec.role, rec.id; + END LOOP; + END IF; +END $$; + +-- ===================================================== +-- SECCIÓN 6: Validación de progress_tracking.module_progress +-- ===================================================== + +\echo '' +\echo '========================================' +\echo 'VALIDACIÓN 6: progress_tracking.module_progress' +\echo '========================================' + +SELECT + '6.1. Total module_progress registros' AS validacion, + COUNT(*) AS cantidad, + '⏭️ INFO (depende de módulos publicados)' AS resultado +FROM progress_tracking.module_progress; + +SELECT + '6.2. Estudiantes CON module_progress' AS validacion, + COUNT(DISTINCT mp.user_id) AS cantidad, + CASE + WHEN COUNT(DISTINCT mp.user_id) >= 16 THEN '✅ OK (mínimo 16 esperados)' + ELSE '⚠️ WARNING: Menos de 16 estudiantes con progreso' + END AS resultado +FROM progress_tracking.module_progress mp +INNER JOIN auth_management.profiles p ON mp.user_id = p.id +WHERE p.role IN ('student', 'admin_teacher', 'super_admin'); + +SELECT + '6.3. Módulos publicados disponibles' AS validacion, + COUNT(*) AS cantidad, + '⏭️ INFO' AS resultado +FROM educational_content.modules +WHERE is_published = true AND status = 'published'; + +-- Mostrar estudiantes sin module_progress (si existen) +DO $$ +DECLARE + profiles_sin_progress INTEGER; +BEGIN + SELECT COUNT(*) INTO profiles_sin_progress + FROM auth_management.profiles p + LEFT JOIN progress_tracking.module_progress mp ON p.id = mp.user_id + WHERE mp.user_id IS NULL + AND p.role IN ('student', 'admin_teacher', 'super_admin'); + + IF profiles_sin_progress > 0 THEN + RAISE NOTICE ''; + RAISE NOTICE '⚠️ PROFILES SIN MODULE_PROGRESS DETECTADOS:'; + FOR rec IN + SELECT p.id, p.email, p.role + FROM auth_management.profiles p + LEFT JOIN progress_tracking.module_progress mp ON p.id = mp.user_id + WHERE mp.user_id IS NULL + AND p.role IN ('student', 'admin_teacher', 'super_admin') + LOOP + RAISE NOTICE ' - % (%, %)', rec.email, rec.role, rec.id; + END LOOP; + END IF; +END $$; + +-- ===================================================== +-- SECCIÓN 7: Resumen Final +-- ===================================================== + +\echo '' +\echo '========================================' +\echo 'RESUMEN FINAL' +\echo '========================================' + +DO $$ +DECLARE + total_users INTEGER; + total_profiles INTEGER; + total_stats INTEGER; + total_inventory INTEGER; + total_ranks INTEGER; + total_progress_users INTEGER; + usuarios_sin_profile INTEGER; + profiles_sin_stats INTEGER; + profiles_sin_inventory INTEGER; + profiles_sin_ranks INTEGER; + profiles_sin_progress INTEGER; + errores_criticos INTEGER := 0; +BEGIN + -- Contar totales + SELECT COUNT(*) INTO total_users FROM auth.users; + SELECT COUNT(*) INTO total_profiles FROM auth_management.profiles; + SELECT COUNT(*) INTO total_stats FROM gamification_system.user_stats; + SELECT COUNT(*) INTO total_inventory FROM gamification_system.comodines_inventory; + SELECT COUNT(*) INTO total_ranks FROM gamification_system.user_ranks; + SELECT COUNT(DISTINCT mp.user_id) INTO total_progress_users + FROM progress_tracking.module_progress mp; + + -- Contar problemas + SELECT COUNT(*) INTO usuarios_sin_profile + FROM auth.users u + LEFT JOIN auth_management.profiles p ON u.id = p.user_id + WHERE p.id IS NULL; + + SELECT COUNT(*) INTO profiles_sin_stats + FROM auth_management.profiles p + LEFT JOIN gamification_system.user_stats us ON p.user_id = us.user_id + WHERE us.user_id IS NULL AND p.role IN ('student', 'admin_teacher', 'super_admin'); + + SELECT COUNT(*) INTO profiles_sin_inventory + FROM auth_management.profiles p + LEFT JOIN gamification_system.comodines_inventory ci ON p.id = ci.user_id + WHERE ci.user_id IS NULL AND p.role IN ('student', 'admin_teacher', 'super_admin'); + + SELECT COUNT(*) INTO profiles_sin_ranks + FROM auth_management.profiles p + LEFT JOIN gamification_system.user_ranks ur ON p.user_id = ur.user_id + WHERE ur.user_id IS NULL AND p.role IN ('student', 'admin_teacher', 'super_admin'); + + SELECT COUNT(*) INTO profiles_sin_progress + FROM auth_management.profiles p + LEFT JOIN progress_tracking.module_progress mp ON p.id = mp.user_id + WHERE mp.user_id IS NULL AND p.role IN ('student', 'admin_teacher', 'super_admin'); + + -- Calcular errores críticos + errores_criticos := usuarios_sin_profile + profiles_sin_stats + + profiles_sin_inventory + profiles_sin_ranks; + + -- Mostrar resumen + RAISE NOTICE ''; + RAISE NOTICE 'TOTALES:'; + RAISE NOTICE ' - auth.users: %', total_users; + RAISE NOTICE ' - auth_management.profiles: %', total_profiles; + RAISE NOTICE ' - gamification_system.user_stats: %', total_stats; + RAISE NOTICE ' - gamification_system.comodines_inventory: %', total_inventory; + RAISE NOTICE ' - gamification_system.user_ranks: %', total_ranks; + RAISE NOTICE ' - progress_tracking.module_progress (usuarios únicos): %', total_progress_users; + RAISE NOTICE ''; + RAISE NOTICE 'PROBLEMAS DETECTADOS:'; + RAISE NOTICE ' - Usuarios sin profile: %', usuarios_sin_profile; + RAISE NOTICE ' - Profiles sin user_stats: %', profiles_sin_stats; + RAISE NOTICE ' - Profiles sin comodines_inventory: %', profiles_sin_inventory; + RAISE NOTICE ' - Profiles sin user_ranks: %', profiles_sin_ranks; + RAISE NOTICE ' - Profiles sin module_progress: % (WARNING)', profiles_sin_progress; + RAISE NOTICE ''; + + IF errores_criticos = 0 THEN + RAISE NOTICE '========================================'; + RAISE NOTICE '✅ VALIDACIÓN EXITOSA'; + RAISE NOTICE '========================================'; + RAISE NOTICE 'Todos los usuarios están completamente inicializados.'; + IF profiles_sin_progress > 0 THEN + RAISE NOTICE '⚠️ WARNING: Hay % usuarios sin module_progress', profiles_sin_progress; + RAISE NOTICE ' (Esto puede ser esperado si no hay módulos publicados)'; + END IF; + ELSE + RAISE NOTICE '========================================'; + RAISE NOTICE '❌ VALIDACIÓN FALLIDA'; + RAISE NOTICE '========================================'; + RAISE NOTICE 'Se detectaron % errores críticos.', errores_criticos; + RAISE NOTICE 'Revisa las secciones anteriores para más detalles.'; + END IF; + + RAISE NOTICE ''; +END $$; + +-- ===================================================== +-- FIN DEL SCRIPT +-- ===================================================== + +\echo '' +\echo 'Validación completa. Revisa los resultados arriba.' +\echo '' diff --git a/projects/gamilit/apps/database/scripts/validations/validate_integrity.py b/projects/gamilit/apps/database/scripts/validations/validate_integrity.py new file mode 100644 index 0000000..9130999 --- /dev/null +++ b/projects/gamilit/apps/database/scripts/validations/validate_integrity.py @@ -0,0 +1,474 @@ +#!/usr/bin/env python3 +""" +Script de validación exhaustiva de integridad de la base de datos GAMILIT +Fecha: 2025-11-07 +""" + +import os +import re +from pathlib import Path +from collections import defaultdict +from typing import Dict, List, Set, Tuple + +# Colores para output +class Colors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + +def print_section(title): + print(f"\n{Colors.HEADER}{Colors.BOLD}{'='*80}{Colors.ENDC}") + print(f"{Colors.HEADER}{Colors.BOLD}{title}{Colors.ENDC}") + print(f"{Colors.HEADER}{Colors.BOLD}{'='*80}{Colors.ENDC}\n") + +def print_error(severity, msg): + color = Colors.FAIL if severity == "CRÍTICO" else Colors.WARNING if severity == "ALTO" else Colors.OKCYAN + print(f"{color}[{severity}] {msg}{Colors.ENDC}") + +def print_ok(msg): + print(f"{Colors.OKGREEN}✓ {msg}{Colors.ENDC}") + +# Configuración de paths (usa variable de entorno o path relativo al script) +BASE_PATH = Path(os.environ.get('GAMILIT_DDL_PATH', + Path(__file__).resolve().parent.parent / 'ddl')) +SCHEMAS_PATH = BASE_PATH / "schemas" + +# 1. EXTRAER TODOS LOS ENUMs DEFINIDOS +def extract_enums(): + """Extrae todos los ENUMs definidos en el sistema""" + enums = {} + + # 1.1 ENUMs en 00-prerequisites.sql + prereq_file = BASE_PATH / "00-prerequisites.sql" + if prereq_file.exists(): + content = prereq_file.read_text() + # Buscar CREATE TYPE schema.enum AS ENUM + pattern = r'CREATE TYPE\s+([\w.]+)\s+AS ENUM\s*\((.*?)\);' + matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE) + for enum_name, values in matches: + schema = "public" + name = enum_name + if "." in enum_name: + schema, name = enum_name.split(".", 1) + + # Limpiar valores + vals = [v.strip().strip("'").strip('"') for v in re.findall(r"'([^']*)'", values)] + enums[f"{schema}.{name}"] = { + "file": str(prereq_file), + "schema": schema, + "name": name, + "values": vals, + "count": len(vals) + } + + # 1.2 ENUMs en archivos individuales + for enum_file in SCHEMAS_PATH.rglob("enums/*.sql"): + content = enum_file.read_text() + + # Extraer schema del path + parts = enum_file.parts + schema_idx = parts.index("schemas") + 1 + schema = parts[schema_idx] + + # Buscar CREATE TYPE + pattern = r'CREATE TYPE\s+([\w.]+)\s+AS ENUM\s*\((.*?)\);' + matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE) + + for enum_name, values in matches: + if "." in enum_name: + schema, name = enum_name.split(".", 1) + else: + name = enum_name + + vals = [v.strip().strip("'").strip('"') for v in re.findall(r"'([^']*)'", values)] + + full_name = f"{schema}.{name}" + enums[full_name] = { + "file": str(enum_file), + "schema": schema, + "name": name, + "values": vals, + "count": len(vals) + } + + return enums + +# 2. EXTRAER TODAS LAS TABLAS DEFINIDAS +def extract_tables(): + """Extrae todas las tablas definidas""" + tables = {} + + for table_file in SCHEMAS_PATH.rglob("tables/*.sql"): + content = table_file.read_text() + + # Extraer schema del path + parts = table_file.parts + schema_idx = parts.index("schemas") + 1 + schema = parts[schema_idx] + + # Buscar CREATE TABLE + pattern = r'CREATE TABLE\s+(?:IF NOT EXISTS\s+)?([\w.]+)' + matches = re.findall(pattern, content, re.IGNORECASE) + + for table_name in matches: + if "." in table_name: + tbl_schema, tbl_name = table_name.split(".", 1) + else: + tbl_schema = schema + tbl_name = table_name + + full_name = f"{tbl_schema}.{tbl_name}" + tables[full_name] = { + "file": str(table_file), + "schema": tbl_schema, + "name": tbl_name + } + + return tables + +# 3. VALIDAR FOREIGN KEYS +def validate_foreign_keys(tables): + """Valida que todas las referencias de FK apunten a tablas existentes""" + issues = [] + + print_section("VALIDACIÓN 1: INTEGRIDAD DE FOREIGN KEYS") + + for table_file in SCHEMAS_PATH.rglob("tables/*.sql"): + content = table_file.read_text() + + # Buscar REFERENCES + pattern = r'REFERENCES\s+([\w.]+)\s*\(' + matches = re.findall(pattern, content, re.IGNORECASE) + + for ref_table in matches: + # Normalizar nombre + if "." not in ref_table: + # Buscar schema del archivo actual + parts = table_file.parts + schema_idx = parts.index("schemas") + 1 + schema = parts[schema_idx] + ref_table = f"{schema}.{ref_table}" + + if ref_table not in tables: + issues.append({ + "severity": "CRÍTICO", + "type": "FK_BROKEN", + "file": str(table_file), + "message": f"Referencia a tabla inexistente: {ref_table}" + }) + + if not issues: + print_ok("Todas las Foreign Keys apuntan a tablas existentes") + else: + for issue in issues: + print_error(issue["severity"], f"{issue['message']}\n Archivo: {issue['file']}") + + return issues + +# 4. VALIDAR ENUMS EN TABLAS +def validate_enum_references(enums, tables): + """Valida que todos los ENUMs usados en tablas existan""" + issues = [] + + print_section("VALIDACIÓN 2: INTEGRIDAD DE ENUMs") + + for table_file in SCHEMAS_PATH.rglob("tables/*.sql"): + content = table_file.read_text() + + # Buscar columnas con tipo ENUM (schema.enum_name) + pattern = r'(\w+)\s+([\w.]+)(?:\s+(?:NOT NULL|DEFAULT|CHECK|UNIQUE|PRIMARY KEY))?' + + # Buscar específicamente tipos que parecen ENUMs (esquema.tipo) + enum_pattern = r'\s+([\w]+\.\w+)(?:\s|,|\))' + enum_matches = re.findall(enum_pattern, content) + + for enum_ref in enum_matches: + # Ignorar cosas que no son ENUMs + if enum_ref.startswith('auth.') or enum_ref.startswith('educational_content.') or enum_ref.startswith('social_features.'): + if '(' in enum_ref or ')' in enum_ref: + continue + + # Verificar si es un ENUM conocido + if enum_ref not in enums: + # Podría ser una tabla, verificar + if enum_ref not in tables: + issues.append({ + "severity": "ALTO", + "type": "ENUM_NOT_FOUND", + "file": str(table_file), + "message": f"Posible referencia a ENUM inexistente: {enum_ref}" + }) + + if not issues: + print_ok("Todos los ENUMs referenciados existen") + else: + for issue in issues: + print_error(issue["severity"], f"{issue['message']}\n Archivo: {issue['file']}") + + return issues + +# 5. VALIDAR CORRECCIONES APLICADAS +def validate_corrections(): + """Valida las correcciones específicas mencionadas en el tracking""" + issues = [] + + print_section("VALIDACIÓN 3: CORRECCIONES APLICADAS") + + # 5.1 notification_type - Debe tener 11 valores + print("\n--- notification_type ---") + enum_file = SCHEMAS_PATH / "public" / "enums" / "notification_type.sql" + if enum_file.exists(): + content = enum_file.read_text() + values = re.findall(r"'([^']*)'", content) + if len(values) == 11: + print_ok(f"notification_type tiene 11 valores correctos") + expected = ['achievement_unlocked', 'rank_up', 'friend_request', 'guild_invitation', + 'mission_completed', 'level_up', 'message_received', 'system_announcement', + 'ml_coins_earned', 'streak_milestone', 'exercise_feedback'] + missing = set(expected) - set(values) + if missing: + issues.append({ + "severity": "ALTO", + "type": "ENUM_VALUES", + "file": str(enum_file), + "message": f"notification_type falta valores: {missing}" + }) + else: + issues.append({ + "severity": "CRÍTICO", + "type": "ENUM_VALUES", + "file": str(enum_file), + "message": f"notification_type tiene {len(values)} valores, esperados 11" + }) + + # 5.2 achievement_category - Debe estar en gamification_system + print("\n--- achievement_category ---") + enum_file = SCHEMAS_PATH / "gamification_system" / "enums" / "achievement_category.sql" + if enum_file.exists(): + print_ok(f"achievement_category está en gamification_system") + + # Verificar que la tabla achievements lo usa + table_file = SCHEMAS_PATH / "gamification_system" / "tables" / "03-achievements.sql" + if table_file.exists(): + content = table_file.read_text() + if "gamification_system.achievement_category" in content: + print_ok("achievements usa gamification_system.achievement_category") + elif "public.achievement_category" in content: + issues.append({ + "severity": "CRÍTICO", + "type": "ENUM_SCHEMA", + "file": str(table_file), + "message": "achievements usa public.achievement_category (debe ser gamification_system)" + }) + else: + issues.append({ + "severity": "CRÍTICO", + "type": "ENUM_MISSING", + "file": "N/A", + "message": "achievement_category no existe en gamification_system" + }) + + # 5.3 transaction_type - Debe estar en gamification_system + print("\n--- transaction_type ---") + enum_file = SCHEMAS_PATH / "gamification_system" / "enums" / "transaction_type.sql" + if enum_file.exists(): + content = enum_file.read_text() + values = re.findall(r"'([^']*)'", content) + print_ok(f"transaction_type existe en gamification_system con {len(values)} valores") + + # Debe tener 14 valores según tracking + if len(values) != 14: + issues.append({ + "severity": "MEDIO", + "type": "ENUM_VALUES", + "file": str(enum_file), + "message": f"transaction_type tiene {len(values)} valores, esperados 14 según tracking" + }) + else: + issues.append({ + "severity": "CRÍTICO", + "type": "ENUM_MISSING", + "file": "N/A", + "message": "transaction_type no existe en gamification_system" + }) + + return issues + +# 6. BUSCAR FUNCIONES CON REFERENCIAS ROTAS +def validate_functions(tables): + """Busca funciones que referencien tablas inexistentes""" + issues = [] + + print_section("VALIDACIÓN 4: FUNCIONES CON REFERENCIAS ROTAS") + + function_files = list(SCHEMAS_PATH.rglob("functions/*.sql")) + + for func_file in function_files: + content = func_file.read_text() + + # Buscar FROM, JOIN, INSERT INTO, UPDATE, DELETE FROM + patterns = [ + r'FROM\s+([\w.]+)', + r'JOIN\s+([\w.]+)', + r'INSERT INTO\s+([\w.]+)', + r'UPDATE\s+([\w.]+)', + r'DELETE FROM\s+([\w.]+)' + ] + + for pattern in patterns: + matches = re.findall(pattern, content, re.IGNORECASE) + for table_ref in matches: + # Normalizar + if "." not in table_ref and table_ref not in ['NEW', 'OLD', 'RETURNING', 'VALUES']: + parts = func_file.parts + schema_idx = parts.index("schemas") + 1 + schema = parts[schema_idx] + table_ref = f"{schema}.{table_ref}" + + if "." in table_ref and table_ref not in tables: + # Verificar que no sea palabra clave SQL + if table_ref.lower() not in ['with.recursive', 'select.distinct']: + issues.append({ + "severity": "ALTO", + "type": "FUNCTION_BROKEN_REF", + "file": str(func_file), + "message": f"Función referencia tabla inexistente: {table_ref}" + }) + + if not issues: + print_ok("Todas las funciones referencian tablas válidas") + else: + for issue in issues: + print_error(issue["severity"], f"{issue['message']}\n Archivo: {issue['file']}") + + return issues + +# 7. BUSCAR TRIGGERS CON REFERENCIAS ROTAS +def validate_triggers(): + """Busca triggers que llamen funciones inexistentes""" + issues = [] + + print_section("VALIDACIÓN 5: TRIGGERS CON REFERENCIAS ROTAS") + + # Primero extraer todas las funciones + functions = set() + for func_file in SCHEMAS_PATH.rglob("functions/*.sql"): + content = func_file.read_text() + pattern = r'CREATE\s+(?:OR REPLACE\s+)?FUNCTION\s+([\w.]+)\s*\(' + matches = re.findall(pattern, content, re.IGNORECASE) + functions.update(matches) + + # Agregar funciones de prerequisites + prereq_file = BASE_PATH / "00-prerequisites.sql" + if prereq_file.exists(): + content = prereq_file.read_text() + pattern = r'CREATE\s+(?:OR REPLACE\s+)?FUNCTION\s+([\w.]+)\s*\(' + matches = re.findall(pattern, content, re.IGNORECASE) + functions.update(matches) + + # Validar triggers + for trigger_file in SCHEMAS_PATH.rglob("triggers/*.sql"): + content = trigger_file.read_text() + + # Buscar EXECUTE FUNCTION + pattern = r'EXECUTE\s+(?:FUNCTION|PROCEDURE)\s+([\w.]+)\s*\(' + matches = re.findall(pattern, content, re.IGNORECASE) + + for func_ref in matches: + if func_ref not in functions: + issues.append({ + "severity": "CRÍTICO", + "type": "TRIGGER_BROKEN_REF", + "file": str(trigger_file), + "message": f"Trigger llama función inexistente: {func_ref}" + }) + + if not issues: + print_ok("Todos los triggers llaman funciones válidas") + else: + for issue in issues: + print_error(issue["severity"], f"{issue['message']}\n Archivo: {issue['file']}") + + return issues + +# 8. BUSCAR ENUMS DUPLICADOS +def check_duplicate_enums(enums): + """Busca ENUMs duplicados en múltiples schemas""" + issues = [] + + print_section("VALIDACIÓN 6: ENUMs DUPLICADOS") + + enum_names = defaultdict(list) + for full_name, info in enums.items(): + name = info["name"] + enum_names[name].append(full_name) + + for name, locations in enum_names.items(): + if len(locations) > 1: + issues.append({ + "severity": "ALTO", + "type": "ENUM_DUPLICATE", + "file": "N/A", + "message": f"ENUM '{name}' duplicado en: {', '.join(locations)}" + }) + + if not issues: + print_ok("No hay ENUMs duplicados") + else: + for issue in issues: + print_error(issue["severity"], issue["message"]) + + return issues + +# MAIN +def main(): + print(f"\n{Colors.BOLD}VALIDACIÓN EXHAUSTIVA DE INTEGRIDAD - BASE DE DATOS GAMILIT{Colors.ENDC}") + print(f"{Colors.BOLD}Fecha: 2025-11-07{Colors.ENDC}") + print(f"{Colors.BOLD}Post-correcciones: 9/142 completadas{Colors.ENDC}\n") + + # Extraer información + print("Extrayendo información de la base de datos...") + enums = extract_enums() + tables = extract_tables() + + print(f"✓ {len(enums)} ENUMs encontrados") + print(f"✓ {len(tables)} tablas encontradas") + + # Ejecutar validaciones + all_issues = [] + + all_issues.extend(validate_foreign_keys(tables)) + all_issues.extend(validate_enum_references(enums, tables)) + all_issues.extend(validate_corrections()) + all_issues.extend(validate_functions(tables)) + all_issues.extend(validate_triggers()) + all_issues.extend(check_duplicate_enums(enums)) + + # RESUMEN FINAL + print_section("RESUMEN DE VALIDACIÓN") + + critical = [i for i in all_issues if i["severity"] == "CRÍTICO"] + high = [i for i in all_issues if i["severity"] == "ALTO"] + medium = [i for i in all_issues if i["severity"] == "MEDIO"] + low = [i for i in all_issues if i["severity"] == "BAJO"] + + print(f"\n{Colors.FAIL}CRÍTICO: {len(critical)} problemas{Colors.ENDC}") + print(f"{Colors.WARNING}ALTO: {len(high)} problemas{Colors.ENDC}") + print(f"{Colors.OKCYAN}MEDIO: {len(medium)} problemas{Colors.ENDC}") + print(f"{Colors.OKBLUE}BAJO: {len(low)} problemas{Colors.ENDC}") + print(f"\n{Colors.BOLD}TOTAL: {len(all_issues)} problemas encontrados{Colors.ENDC}\n") + + if len(all_issues) == 0: + print(f"{Colors.OKGREEN}{Colors.BOLD}✓✓✓ BASE DE DATOS VALIDADA EXITOSAMENTE ✓✓✓{Colors.ENDC}\n") + else: + print(f"{Colors.FAIL}{Colors.BOLD}⚠ SE REQUIERE ATENCIÓN ⚠{Colors.ENDC}\n") + + return all_issues + +if __name__ == "__main__": + issues = main() diff --git a/projects/gamilit/apps/frontend/src/services/api/missionsAPI.ts b/projects/gamilit/apps/frontend/src/services/api/missionsAPI.ts index 6623493..a23c702 100644 --- a/projects/gamilit/apps/frontend/src/services/api/missionsAPI.ts +++ b/projects/gamilit/apps/frontend/src/services/api/missionsAPI.ts @@ -15,6 +15,7 @@ */ import { apiClient } from '@/services/api/apiClient'; +import { handleAPIError } from '@/services/api/apiErrorHandler'; /** * @deprecated Usar Mission de @/features/gamification/missions/types/missionsTypes.ts @@ -56,47 +57,71 @@ export const missionsAPI = { * Get 3 daily missions (auto-generates if needed) */ getDailyMissions: async (): Promise => { - const response = await apiClient.get('/gamification/missions/daily'); - return response.data.data.missions; + try { + const response = await apiClient.get('/gamification/missions/daily'); + return response.data.data.missions; + } catch (error) { + throw handleAPIError(error); + } }, /** * Get 5 weekly missions (auto-generates if needed) */ getWeeklyMissions: async (): Promise => { - const response = await apiClient.get('/gamification/missions/weekly'); - return response.data.data.missions; + try { + const response = await apiClient.get('/gamification/missions/weekly'); + return response.data.data.missions; + } catch (error) { + throw handleAPIError(error); + } }, /** * Get active special missions (events) */ getSpecialMissions: async (): Promise => { - const response = await apiClient.get('/gamification/missions/special'); - return response.data.data.missions; + try { + const response = await apiClient.get('/gamification/missions/special'); + return response.data.data.missions; + } catch (error) { + throw handleAPIError(error); + } }, /** * Claim mission rewards */ claimRewards: async (missionId: string) => { - const response = await apiClient.post(`/gamification/missions/${missionId}/claim`); - return response.data.data; + try { + const response = await apiClient.post(`/gamification/missions/${missionId}/claim`); + return response.data.data; + } catch (error) { + throw handleAPIError(error); + } }, /** * Get mission progress */ getMissionProgress: async (missionId: string) => { - const response = await apiClient.get(`/gamification/missions/${missionId}/progress`); - return response.data.data; + try { + const response = await apiClient.get(`/gamification/missions/${missionId}/progress`); + return response.data.data; + } catch (error) { + throw handleAPIError(error); + } }, /** * Get user mission statistics */ getMissionStats: async (userId: string) => { - const response = await apiClient.get(`/gamification/missions/stats/${userId}`); - return response.data.data; + try { + const response = await apiClient.get(`/gamification/missions/stats/${userId}`); + return response.data.data; + } catch (error) { + throw handleAPIError(error); + } }, }; diff --git a/projects/gamilit/apps/frontend/src/services/api/passwordAPI.ts b/projects/gamilit/apps/frontend/src/services/api/passwordAPI.ts index c2558a5..fdd5173 100644 --- a/projects/gamilit/apps/frontend/src/services/api/passwordAPI.ts +++ b/projects/gamilit/apps/frontend/src/services/api/passwordAPI.ts @@ -8,6 +8,7 @@ */ import { apiClient } from './apiClient'; +import { handleAPIError } from './apiErrorHandler'; // ============================================================================ // TYPES @@ -71,8 +72,12 @@ export const passwordAPI = { * ``` */ requestPasswordReset: async (email: string): Promise => { - const response = await apiClient.post('/auth/reset-password/request', { email }); - return response.data; + try { + const response = await apiClient.post('/auth/reset-password/request', { email }); + return response.data; + } catch (error) { + throw handleAPIError(error); + } }, /** @@ -90,11 +95,15 @@ export const passwordAPI = { * ``` */ resetPassword: async (token: string, newPassword: string): Promise => { - const response = await apiClient.post('/auth/reset-password', { - token, - new_password: newPassword, - }); - return response.data; + try { + const response = await apiClient.post('/auth/reset-password', { + token, + new_password: newPassword, + }); + return response.data; + } catch (error) { + throw handleAPIError(error); + } }, /** diff --git a/projects/gamilit/apps/frontend/src/services/api/profileAPI.ts b/projects/gamilit/apps/frontend/src/services/api/profileAPI.ts index f17a19e..bb36fdb 100644 --- a/projects/gamilit/apps/frontend/src/services/api/profileAPI.ts +++ b/projects/gamilit/apps/frontend/src/services/api/profileAPI.ts @@ -9,6 +9,7 @@ */ import { apiClient } from './apiClient'; +import { handleAPIError } from './apiErrorHandler'; // ============================================================================ // TYPES @@ -101,8 +102,12 @@ export const profileAPI = { * @returns Updated profile data */ updateProfile: async (userId: string, data: UpdateProfileDto): Promise => { - const response = await apiClient.put(`/users/${userId}/profile`, data); - return response.data; + try { + const response = await apiClient.put(`/users/${userId}/profile`, data); + return response.data; + } catch (error) { + throw handleAPIError(error); + } }, /** @@ -111,8 +116,12 @@ export const profileAPI = { * @returns User preferences data */ getPreferences: async (): Promise<{ preferences: Record }> => { - const response = await apiClient.get('/users/preferences'); - return response.data; + try { + const response = await apiClient.get('/users/preferences'); + return response.data; + } catch (error) { + throw handleAPIError(error); + } }, /** @@ -126,8 +135,12 @@ export const profileAPI = { userId: string, preferences: UpdatePreferencesDto, ): Promise => { - const response = await apiClient.put(`/users/${userId}/preferences`, { preferences }); - return response.data; + try { + const response = await apiClient.put(`/users/${userId}/preferences`, { preferences }); + return response.data; + } catch (error) { + throw handleAPIError(error); + } }, /** @@ -138,14 +151,18 @@ export const profileAPI = { * @returns Avatar URL */ uploadAvatar: async (userId: string, file: File): Promise => { - const formData = new FormData(); - formData.append('avatar', file); + try { + const formData = new FormData(); + formData.append('avatar', file); - const response = await apiClient.post(`/users/${userId}/avatar`, formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }); + const response = await apiClient.post(`/users/${userId}/avatar`, formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); - return response.data; + return response.data; + } catch (error) { + throw handleAPIError(error); + } }, /** @@ -159,8 +176,12 @@ export const profileAPI = { userId: string, passwords: UpdatePasswordDto, ): Promise => { - const response = await apiClient.put(`/users/${userId}/password`, passwords); - return response.data; + try { + const response = await apiClient.put(`/users/${userId}/password`, passwords); + return response.data; + } catch (error) { + throw handleAPIError(error); + } }, }; diff --git a/projects/gamilit/apps/frontend/src/shared/components/mechanics/ExerciseContentRenderer.tsx b/projects/gamilit/apps/frontend/src/shared/components/mechanics/ExerciseContentRenderer.tsx index 6b4ee77..481da7a 100644 --- a/projects/gamilit/apps/frontend/src/shared/components/mechanics/ExerciseContentRenderer.tsx +++ b/projects/gamilit/apps/frontend/src/shared/components/mechanics/ExerciseContentRenderer.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { FileText, CheckCircle, XCircle, Music, Type, Grid3X3, ListChecks } from 'lucide-react'; +import { FileText, CheckCircle, XCircle, Music, Type, Grid3X3, ListChecks, Link2 } from 'lucide-react'; interface ExerciseContentRendererProps { exerciseType: string; @@ -35,10 +35,22 @@ export const ExerciseContentRenderer: React.FC = ( return ; case 'verdadero_falso': - return ; + return ( + + ); case 'completar_espacios': - return ; + return ( + + ); case 'crucigrama': return ; @@ -52,13 +64,28 @@ export const ExerciseContentRenderer: React.FC = ( case 'timeline': return ; + case 'emparejamiento': + return ( + + ); + // Módulo 2 - Automáticos (opción múltiple) case 'lectura_inferencial': case 'puzzle_contexto': case 'detective_textual': case 'rueda_inferencias': case 'causa_efecto': - return ; + return ( + + ); // Módulo 2 - Manuales (texto abierto) // P0-03: Moved prediccion_narrativa to TextResponseRenderer (2025-12-18) @@ -114,7 +141,7 @@ const PodcastRenderer: React.FC<{ data: Record }> = ({ data }) return (
-
+
Tema seleccionado
@@ -122,16 +149,16 @@ const PodcastRenderer: React.FC<{ data: Record }> = ({ data })
-
+
Guión del Podcast
-

{script}

+

{script}

{audioUrl && (
-
+
Audio del Podcast
@@ -188,16 +215,19 @@ const VerdaderoFalsoRenderer: React.FC<{ const rawCorrectAnswers = correct?.statements || correct?.answers || correct; const correctAnswers: Record | undefined = rawCorrectAnswers - ? Object.entries(rawCorrectAnswers as Record).reduce((acc, [key, val]) => { - if (typeof val === 'string') { - acc[key] = val.toLowerCase() === 'true'; - } else if (typeof val === 'boolean') { - acc[key] = val; - } else { - acc[key] = Boolean(val); - } - return acc; - }, {} as Record) + ? Object.entries(rawCorrectAnswers as Record).reduce( + (acc, [key, val]) => { + if (typeof val === 'string') { + acc[key] = val.toLowerCase() === 'true'; + } else if (typeof val === 'boolean') { + acc[key] = val; + } else { + acc[key] = Boolean(val); + } + return acc; + }, + {} as Record, + ) : undefined; console.log('[VerdaderoFalsoRenderer] Normalized:', { answers, correctAnswers }); @@ -212,8 +242,8 @@ const VerdaderoFalsoRenderer: React.FC<{ className={`flex items-center gap-3 rounded-lg p-3 ${ showComparison && isCorrect !== undefined ? isCorrect - ? 'bg-green-50 border border-green-200' - : 'bg-red-50 border border-red-200' + ? 'border border-green-200 bg-green-50' + : 'border border-red-200 bg-red-50' : 'bg-gray-50' }`} > @@ -225,7 +255,7 @@ const VerdaderoFalsoRenderer: React.FC<{ Pregunta {key}: {value ? 'Verdadero' : 'Falso'} {showComparison && isCorrect === false && correctAnswers && ( - + (Correcto: {correctAnswers[key] ? 'Verdadero' : 'Falso'}) )} @@ -260,17 +290,15 @@ const CompletarEspaciosRenderer: React.FC<{ className={`flex items-center gap-3 rounded-lg p-3 ${ showComparison && isCorrect !== undefined ? isCorrect - ? 'bg-green-50 border border-green-200' - : 'bg-red-50 border border-red-200' + ? 'border border-green-200 bg-green-50' + : 'border border-red-200 bg-red-50' : 'bg-gray-50' }`} > Espacio {key}: - {value || '(vacío)'} + {value || '(vacío)'} {showComparison && isCorrect === false && correctBlanks && ( - - → {correctBlanks[key]} - + → {correctBlanks[key]} )}
); @@ -288,13 +316,13 @@ const CrucigramaRenderer: React.FC<{ data: Record }> = ({ data return (
-
+
Palabras del Crucigrama
{Object.entries(words).map(([key, value]) => ( -
+
{key}: {value}
@@ -313,13 +341,13 @@ const SopaLetrasRenderer: React.FC<{ data: Record }> = ({ data return (
-
+
Palabras Encontradas ({foundWords.length})
{foundWords.map((word, idx) => ( - + {word} ))} @@ -333,19 +361,27 @@ const SopaLetrasRenderer: React.FC<{ data: Record }> = ({ data * Muestra las conexiones entre nodos */ const MapaConceptualRenderer: React.FC<{ data: Record }> = ({ data }) => { - const connections = (data.connections || data.nodes || []) as Array<{from?: string; to?: string; label?: string}>; + const connections = (data.connections || data.nodes || []) as Array<{ + from?: string; + to?: string; + label?: string; + }>; return (
- Conexiones del Mapa Conceptual + Conexiones del Mapa Conceptual
- {Array.isArray(connections) ? connections.map((conn, idx) => ( -
- {conn.from || `Nodo ${idx}`} - - {conn.to || conn.label || 'conecta'} -
- )) : ( + {Array.isArray(connections) ? ( + connections.map((conn, idx) => ( +
+ {conn.from || `Nodo ${idx}`} + + + {conn.to || conn.label || 'conecta'} + +
+ )) + ) : (
{JSON.stringify(data, null, 2)}
)}
@@ -358,20 +394,26 @@ const MapaConceptualRenderer: React.FC<{ data: Record }> = ({ d * Muestra los eventos en orden cronológico */ const TimelineRenderer: React.FC<{ data: Record }> = ({ data }) => { - const events = (data.events || data.order || []) as Array<{id?: string; position?: number; text?: string}>; + const events = (data.events || data.order || []) as Array<{ + id?: string; + position?: number; + text?: string; + }>; return (
- Orden de Eventos + Orden de Eventos
- {Array.isArray(events) ? events.map((event, idx) => ( -
- - {event.position || idx + 1} - - {event.text || event.id || `Evento ${idx + 1}`} -
- )) : ( + {Array.isArray(events) ? ( + events.map((event, idx) => ( +
+ + {event.position || idx + 1} + + {event.text || event.id || `Evento ${idx + 1}`} +
+ )) + ) : (
{JSON.stringify(data, null, 2)}
)}
@@ -379,6 +421,70 @@ const TimelineRenderer: React.FC<{ data: Record }> = ({ data }) ); }; +/** + * Renderiza respuestas del ejercicio Emparejamiento + * Muestra los pares que el estudiante conectó + */ +const EmparejamientoRenderer: React.FC<{ + data: Record; + correct?: Record; + showComparison: boolean; +}> = ({ data, correct, showComparison }) => { + // El formato de respuesta es { matches: { questionId: answerId } } + const matches = (data.matches || data) as Record; + const correctMatches = (correct?.matches || correct) as Record | undefined; + + return ( +
+
+ + Emparejamientos Realizados +
+
+ {Object.entries(matches).map(([questionId, answerId]) => { + const isCorrect = correctMatches + ? correctMatches[questionId] === answerId + : undefined; + return ( +
+ + {questionId} + + + + {answerId} + + {showComparison && isCorrect !== undefined && ( + isCorrect ? ( + + ) : ( + <> + + {correctMatches && ( + + → {correctMatches[questionId]} + + )} + + ) + )} +
+ ); + })} +
+
+ ); +}; + /** * Renderiza respuestas de ejercicios de opción múltiple * Usado para ejercicios del Módulo 2 (inferenciales) @@ -401,14 +507,14 @@ const MultipleChoiceRenderer: React.FC<{ className={`rounded-lg p-3 ${ showComparison && isCorrect !== undefined ? isCorrect - ? 'bg-green-50 border border-green-200' - : 'bg-red-50 border border-red-200' + ? 'border border-green-200 bg-green-50' + : 'border border-red-200 bg-red-50' : 'bg-gray-50' }`} > {key}: {String(value)} {showComparison && isCorrect === false && correctAnswers && ( - + (Correcto: {String(correctAnswers[key])}) )} @@ -428,10 +534,10 @@ const TextResponseRenderer: React.FC<{ data: Record }> = ({ dat
{Object.entries(data).map(([key, value]) => (
- + {key.replace(/_/g, ' ')} -

+

{typeof value === 'string' ? value : JSON.stringify(value, null, 2)}

@@ -445,7 +551,10 @@ const TextResponseRenderer: React.FC<{ data: Record }> = ({ dat * Usado para ejercicios de Módulos 4 y 5 (creativos) * Detecta y renderiza imágenes, videos y audio inline */ -const MultimediaRenderer: React.FC<{ data: Record; type: string }> = ({ data, type: _type }) => { +const MultimediaRenderer: React.FC<{ data: Record; type: string }> = ({ + data, + type: _type, +}) => { return (
{Object.entries(data).map(([key, value]) => { @@ -457,12 +566,12 @@ const MultimediaRenderer: React.FC<{ data: Record; type: string return (
- + {key.replace(/_/g, ' ')} {isImageUrl && typeof value === 'string' ? ( - {key} + {key} ) : isVideoUrl && typeof value === 'string' ? (