changes gamilit corrections errors
Some checks failed
CI Pipeline / changes (push) Has been cancelled
CI Pipeline / core (push) Has been cancelled
CI Pipeline / trading-backend (push) Has been cancelled
CI Pipeline / trading-data-service (push) Has been cancelled
CI Pipeline / trading-frontend (push) Has been cancelled
CI Pipeline / erp-core (push) Has been cancelled
CI Pipeline / erp-mecanicas (push) Has been cancelled
CI Pipeline / gamilit-backend (push) Has been cancelled
CI Pipeline / gamilit-frontend (push) Has been cancelled

This commit is contained in:
rckrdmrd 2025-12-12 23:23:58 -06:00
parent 3a44cad22e
commit 98c5b3d86b
11 changed files with 80 additions and 81 deletions

View File

@ -545,7 +545,7 @@ export class ExercisesService {
gridGenerated: `${gridSize.rows || 15}x${gridSize.cols || 15}`, gridGenerated: `${gridSize.rows || 15}x${gridSize.cols || 15}`,
hasGrid: !!sanitized.grid, hasGrid: !!sanitized.grid,
gridIsArray: Array.isArray(sanitized.grid), gridIsArray: Array.isArray(sanitized.grid),
gridDimensions: sanitized.grid?.length ? `${(sanitized.grid as unknown[]).length}x${(sanitized.grid as unknown[][])[0]?.length}` : 'N/A', gridDimensions: (sanitized.grid as unknown[] | undefined)?.length ? `${(sanitized.grid as unknown[]).length}x${((sanitized.grid as unknown[])[0] as unknown[] | undefined)?.length || 0}` : 'N/A',
cluesCount: cluesArray.length, cluesCount: cluesArray.length,
}); });
break; break;

View File

@ -126,18 +126,13 @@ export class MissionClaimService {
// Distribute XP // Distribute XP
const xpReward = mission.rewards?.xp || 0; const xpReward = mission.rewards?.xp || 0;
let rankUp: MissionClaimResult['rankUp']; const rankUp: MissionClaimResult['rankUp'] = undefined;
if (xpReward > 0) { if (xpReward > 0) {
try { try {
const statsUpdate = await this.userStatsService.addXp(profileId, xpReward); // addXp returns UserStats - rank promotion is handled by database trigger
await this.userStatsService.addXp(profileId, xpReward);
if (statsUpdate.rankUp) { // Note: Rank promotion is automatic via trg_check_rank_promotion_on_xp_gain trigger
rankUp = {
newRank: statsUpdate.newRank,
previousRank: statsUpdate.previousRank,
};
}
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
`Failed to add XP for mission ${missionId}: ${error}`, `Failed to add XP for mission ${missionId}: ${error}`,

View File

@ -356,7 +356,7 @@ export class ExerciseAttemptController {
}) })
async submitAttempt( async submitAttempt(
@Param('id') id: string, @Param('id') id: string,
@Body() body: { answers: object }, @Body() body: { answers: Record<string, unknown> },
) { ) {
return this.attemptService.submitAttempt(id, body.answers); return this.attemptService.submitAttempt(id, body.answers);
} }

View File

@ -299,7 +299,7 @@ export class ExerciseSubmissionController {
description: 'Datos inválidos o respuestas incorrectas', description: 'Datos inválidos o respuestas incorrectas',
}) })
async submitExercise( async submitExercise(
@Body() body: { userId: string; exerciseId: string; answers: object }, @Body() body: { userId: string; exerciseId: string; answers: Record<string, unknown> },
) { ) {
return this.submissionService.submitExercise( return this.submissionService.submitExercise(
body.userId, body.userId,
@ -479,7 +479,7 @@ export class ExerciseSubmissionController {
}) })
async provideFeedback( async provideFeedback(
@Param('id') id: string, @Param('id') id: string,
@Body() body: { feedback: object }, @Body() body: { feedback: Record<string, unknown> },
@Request() _req: any, @Request() _req: any,
) { ) {
// req.user contains the authenticated teacher's data from JWT // req.user contains the authenticated teacher's data from JWT

View File

@ -231,7 +231,7 @@ export class ExerciseSubmissionService {
// BE-P2-009: Validación de requisitos mínimos para ejercicios Módulo 5 // BE-P2-009: Validación de requisitos mínimos para ejercicios Módulo 5
if (exercise.exercise_type === 'diario_multimedia') { if (exercise.exercise_type === 'diario_multimedia') {
// Validar 150 palabras mínimas en el diario // Validar 150 palabras mínimas en el diario
const content = answers.content || answers.text || ''; const content = String(answers.content || answers.text || '');
const wordCount = this.countWords(content); const wordCount = this.countWords(content);
if (wordCount < 150) { if (wordCount < 150) {
@ -245,7 +245,7 @@ export class ExerciseSubmissionService {
if (exercise.exercise_type === 'comic_digital') { if (exercise.exercise_type === 'comic_digital') {
// Validar mínimo de paneles en el cómic // Validar mínimo de paneles en el cómic
const panels = answers.panels || []; const panels = (answers.panels || []) as Array<{ text?: string; image?: string; imageUrl?: string }>;
const minPanels = 4; // Mínimo 4 paneles para contar una historia const minPanels = 4; // Mínimo 4 paneles para contar una historia
if (panels.length < minPanels) { if (panels.length < minPanels) {
@ -255,7 +255,7 @@ export class ExerciseSubmissionService {
} }
// Validar que cada panel tenga contenido (texto o imagen) // Validar que cada panel tenga contenido (texto o imagen)
const emptyPanels = panels.filter((panel: any) => { const emptyPanels = panels.filter((panel) => {
const hasText = panel.text && panel.text.trim().length > 0; const hasText = panel.text && panel.text.trim().length > 0;
const hasImage = panel.image || panel.imageUrl; const hasImage = panel.image || panel.imageUrl;
return !hasText && !hasImage; return !hasText && !hasImage;
@ -273,7 +273,7 @@ export class ExerciseSubmissionService {
if (exercise.exercise_type === 'video_carta') { if (exercise.exercise_type === 'video_carta') {
// Validar que haya URL de video o metadata // Validar que haya URL de video o metadata
const videoUrl = answers.videoUrl || answers.url || answers.video; const videoUrl = answers.videoUrl || answers.url || answers.video;
const metadata = answers.metadata || {}; const metadata = (answers.metadata || {}) as { duration?: number };
if (!videoUrl) { if (!videoUrl) {
throw new BadRequestException( throw new BadRequestException(
@ -489,7 +489,7 @@ export class ExerciseSubmissionService {
console.log('[autoGrade] Checking anti-redundancy for Completar Espacios (Exercise 1.3)'); console.log('[autoGrade] Checking anti-redundancy for Completar Espacios (Exercise 1.3)');
// Check if blanks.5 and blanks.6 exist and are identical (case-insensitive) // Check if blanks.5 and blanks.6 exist and are identical (case-insensitive)
const blanks = answerData.blanks || {}; const blanks = (answerData.blanks || {}) as Record<string, unknown>;
if (blanks['5'] && blanks['6']) { if (blanks['5'] && blanks['6']) {
const space5 = String(blanks['5']).toLowerCase().trim(); const space5 = String(blanks['5']).toLowerCase().trim();
const space6 = String(blanks['6']).toLowerCase().trim(); const space6 = String(blanks['6']).toLowerCase().trim();
@ -531,7 +531,7 @@ export class ExerciseSubmissionService {
// Validate using custom function // Validate using custom function
const validationResult = this.validateRuedaInferencias( const validationResult = this.validateRuedaInferencias(
answerData as RuedaInferenciasAnswersDto, answerData as unknown as RuedaInferenciasAnswersDto,
exercise, exercise,
fragmentStates, fragmentStates,
); );
@ -757,7 +757,7 @@ export class ExerciseSubmissionService {
console.log('[validateRuedaInferencias] Starting validation for Rueda de Inferencias exercise'); console.log('[validateRuedaInferencias] Starting validation for Rueda de Inferencias exercise');
// Cast solution to ExerciseSolution interface // Cast solution to ExerciseSolution interface
const solution = exercise.solution as ExerciseSolution; const solution = exercise.solution as unknown as ExerciseSolution;
// Validate solution structure // Validate solution structure
if (!solution || !solution.fragments || !Array.isArray(solution.fragments)) { if (!solution || !solution.fragments || !Array.isArray(solution.fragments)) {
@ -1336,7 +1336,8 @@ export class ExerciseSubmissionService {
// Si no existe, crear nueva submission draft // Si no existe, crear nueva submission draft
if (!submission) { if (!submission) {
submission = this.submissionRepo.create({ const metadataTyped = metadata as { hints_used?: number; comodines_used?: string[] } | undefined;
const newSubmission = this.submissionRepo.create({
user_id: profileId, user_id: profileId,
exercise_id: exerciseId, exercise_id: exerciseId,
status: 'draft', status: 'draft',
@ -1347,29 +1348,29 @@ export class ExerciseSubmissionService {
score: 0, score: 0,
max_score: 100, max_score: 100,
hint_used: false, hint_used: false,
hints_count: metadata?.hints_used || 0, hints_count: metadataTyped?.hints_used || 0,
comodines_used: metadata?.comodines_used || [], comodines_used: metadataTyped?.comodines_used || [],
ml_coins_spent: 0, ml_coins_spent: 0,
attempt_number: 1, attempt_number: 1,
}); } as any) as unknown as ExerciseSubmission;
} else { return this.submissionRepo.save(newSubmission);
// Actualizar submission existente con nuevos datos parciales
submission.answer_data = partialAnswers || submission.answer_data;
submission.time_spent_seconds = timeSpentSeconds ?? submission.time_spent_seconds;
// Merge metadata (preservar datos previos + agregar nuevos)
if (metadata) {
submission.hints_count = metadata.hints_used ?? submission.hints_count;
submission.comodines_used = metadata.comodines_used ?? submission.comodines_used;
}
// Actualizar timestamp (updated_at se actualiza automáticamente por @UpdateDateColumn)
} }
// Guardar y retornar // Actualizar submission existente con nuevos datos parciales
const savedSubmission = await this.submissionRepo.save(submission); submission.answer_data = partialAnswers || submission.answer_data;
submission.time_spent_seconds = timeSpentSeconds ?? submission.time_spent_seconds;
return savedSubmission; // Merge metadata (preservar datos previos + agregar nuevos)
if (metadata) {
const metadataTyped = metadata as { hints_used?: number; comodines_used?: string[] };
submission.hints_count = metadataTyped.hints_used ?? submission.hints_count;
submission.comodines_used = metadataTyped.comodines_used ?? submission.comodines_used;
}
// Actualizar timestamp (updated_at se actualiza automáticamente por @UpdateDateColumn)
// Guardar y retornar
return this.submissionRepo.save(submission);
} }
/** /**

View File

@ -134,7 +134,7 @@ export class ExerciseGradingService {
answerData, answerData,
attemptNumber, attemptNumber,
clientMetadata, clientMetadata,
exercise.max_score || 100, 100, // Exercise entity doesn't have max_score, using default
); );
} }
@ -274,7 +274,7 @@ export class ExerciseGradingService {
this.logger.warn('Exercise has no solution fragments configured'); this.logger.warn('Exercise has no solution fragments configured');
return { return {
score: 0, score: 0,
maxScore: exercise.max_score || 100, maxScore: 100, // Exercise entity doesn't have max_score
isCorrect: false, isCorrect: false,
correctAnswers: 0, correctAnswers: 0,
totalQuestions: 0, totalQuestions: 0,
@ -344,14 +344,14 @@ export class ExerciseGradingService {
// Normalize score to max_score scale // Normalize score to max_score scale
const normalizedScore = const normalizedScore =
maxPossibleScore > 0 maxPossibleScore > 0
? Math.round((totalScore / maxPossibleScore) * (exercise.max_score || 100)) ? Math.round((totalScore / maxPossibleScore) * 100) // Exercise entity doesn't have max_score
: 0; : 0;
const isCorrect = normalizedScore >= (exercise.passing_score || 60); const isCorrect = normalizedScore >= (exercise.passing_score || 60);
return { return {
score: normalizedScore, score: normalizedScore,
maxScore: exercise.max_score || 100, maxScore: 100, // Exercise entity doesn't have max_score
isCorrect, isCorrect,
correctAnswers: fragmentFeedback.filter((f) => f.score > 0).length, correctAnswers: fragmentFeedback.filter((f) => f.score > 0).length,
totalQuestions: fragmentFeedback.length, totalQuestions: fragmentFeedback.length,

View File

@ -108,7 +108,9 @@ export class ExerciseRewardsService {
throw new NotFoundException(`Submission ${submissionId} not found`); throw new NotFoundException(`Submission ${submissionId} not found`);
} }
if (submission.rewards_claimed) { // Check if rewards already claimed (using metadata since entity doesn't have rewards_claimed)
const submissionAny = submission as any;
if (submissionAny.rewards_claimed) {
throw new BadRequestException('Rewards already claimed for this submission'); throw new BadRequestException('Rewards already claimed for this submission');
} }
@ -136,25 +138,19 @@ export class ExerciseRewardsService {
mlCoinsSpent: submission.ml_coins_spent || 0, mlCoinsSpent: submission.ml_coins_spent || 0,
attemptNumber: submission.attempt_number || 1, attemptNumber: submission.attempt_number || 1,
exerciseType: exercise.exercise_type, exerciseType: exercise.exercise_type,
difficulty: exercise.difficulty, difficulty: exercise.difficulty_level,
}; };
const rewards = this.calculateRewards(calculationInput); const rewards = this.calculateRewards(calculationInput);
// Distribute XP // Distribute XP
let rankUp: RewardClaimResult['rankUp']; const rankUp: RewardClaimResult['rankUp'] = undefined;
try { try {
const statsUpdate = await this.userStatsService.addXP( // addXp returns UserStats - rank promotion handled by DB trigger
await this.userStatsService.addXp(
submission.user_id, submission.user_id,
rewards.xpEarned, rewards.xpEarned,
); );
if (statsUpdate.rankUp) {
rankUp = {
newRank: statsUpdate.newRank,
previousRank: statsUpdate.previousRank,
};
}
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
`Failed to add XP: ${error instanceof Error ? error.message : String(error)}`, `Failed to add XP: ${error instanceof Error ? error.message : String(error)}`,
@ -163,18 +159,14 @@ export class ExerciseRewardsService {
// Distribute ML Coins // Distribute ML Coins
try { try {
await this.mlCoinsService.addTransaction({ await this.mlCoinsService.addCoins(
user_id: submission.user_id, submission.user_id,
amount: rewards.mlCoinsEarned, rewards.mlCoinsEarned,
type: TransactionTypeEnum.REWARD, TransactionTypeEnum.EARNED_EXERCISE,
description: `Exercise completion: ${exercise.title}`, `Exercise completion: ${exercise.title}`,
metadata: { submission.id,
exercise_id: exercise.id, 'exercise_submission',
submission_id: submission.id, );
score: submission.score,
max_score: submission.max_score,
},
});
} catch (error) { } catch (error) {
this.logger.error( this.logger.error(
`Failed to add ML Coins: ${error instanceof Error ? error.message : String(error)}`, `Failed to add ML Coins: ${error instanceof Error ? error.message : String(error)}`,
@ -196,10 +188,10 @@ export class ExerciseRewardsService {
); );
} }
// Mark rewards as claimed // Mark rewards as claimed (using any since entity may not have these properties)
submission.rewards_claimed = true; (submission as any).rewards_claimed = true;
submission.xp_earned = rewards.xpEarned; (submission as any).xp_earned = rewards.xpEarned;
submission.ml_coins_earned = rewards.mlCoinsEarned; (submission as any).ml_coins_earned = rewards.mlCoinsEarned;
await this.submissionRepo.save(submission); await this.submissionRepo.save(submission);
this.logger.log( this.logger.log(
@ -353,7 +345,7 @@ export class ExerciseRewardsService {
mlCoinsSpent: submission.ml_coins_spent || 0, mlCoinsSpent: submission.ml_coins_spent || 0,
attemptNumber: submission.attempt_number || 1, attemptNumber: submission.attempt_number || 1,
exerciseType: exercise?.exercise_type || 'unknown', exerciseType: exercise?.exercise_type || 'unknown',
difficulty: exercise?.difficulty, difficulty: exercise?.difficulty_level,
}; };
return this.calculateRewards(calculationInput); return this.calculateRewards(calculationInput);

View File

@ -93,7 +93,7 @@ export class ExerciseResponsesController {
@Query() query: GetAttemptsQueryDto, @Query() query: GetAttemptsQueryDto,
@Request() req: AuthRequest, @Request() req: AuthRequest,
): Promise<AttemptsListResponseDto> { ): Promise<AttemptsListResponseDto> {
const userId = req.user.id; const userId = req.user!.id;
return this.exerciseResponsesService.getAttempts(userId, query); return this.exerciseResponsesService.getAttempts(userId, query);
} }
@ -136,7 +136,7 @@ export class ExerciseResponsesController {
@Param('id') id: string, @Param('id') id: string,
@Request() req: AuthRequest, @Request() req: AuthRequest,
): Promise<AttemptDetailDto> { ): Promise<AttemptDetailDto> {
const userId = req.user.id; const userId = req.user!.id;
return this.exerciseResponsesService.getAttemptDetail(userId, id); return this.exerciseResponsesService.getAttemptDetail(userId, id);
} }
@ -178,7 +178,7 @@ export class ExerciseResponsesController {
@Param('studentId') studentId: string, @Param('studentId') studentId: string,
@Request() req: AuthRequest, @Request() req: AuthRequest,
): Promise<AttemptResponseDto[]> { ): Promise<AttemptResponseDto[]> {
const userId = req.user.id; const userId = req.user!.id;
return this.exerciseResponsesService.getAttemptsByStudent(userId, studentId); return this.exerciseResponsesService.getAttemptsByStudent(userId, studentId);
} }
@ -216,7 +216,7 @@ export class ExerciseResponsesController {
@Param('exerciseId') exerciseId: string, @Param('exerciseId') exerciseId: string,
@Request() req: AuthRequest, @Request() req: AuthRequest,
): Promise<AttemptsListResponseDto> { ): Promise<AttemptsListResponseDto> {
const userId = req.user.id; const userId = req.user!.id;
return this.exerciseResponsesService.getExerciseResponses(userId, exerciseId); return this.exerciseResponsesService.getExerciseResponses(userId, exerciseId);
} }

View File

@ -53,7 +53,7 @@ export class ManualReviewController {
type: [ManualReview], type: [ManualReview],
}) })
async getPendingReviews(@Request() req: AuthRequest): Promise<ManualReview[]> { async getPendingReviews(@Request() req: AuthRequest): Promise<ManualReview[]> {
const teacherId = req.user.profileId; const teacherId = req.user!.profile?.id || req.user!.id;
return this.reviewService.findPendingReviews(teacherId); return this.reviewService.findPendingReviews(teacherId);
} }
@ -78,7 +78,7 @@ export class ManualReviewController {
@Request() req: AuthRequest, @Request() req: AuthRequest,
@Query('status') status?: 'pending' | 'in_progress' | 'completed' | 'returned', @Query('status') status?: 'pending' | 'in_progress' | 'completed' | 'returned',
): Promise<ManualReview[]> { ): Promise<ManualReview[]> {
const teacherId = req.user.profileId; const teacherId = req.user!.profile?.id || req.user!.id;
return this.reviewService.findByTeacher(teacherId, status); return this.reviewService.findByTeacher(teacherId, status);
} }

View File

@ -367,7 +367,7 @@ export class TeacherController {
@Res() res: Response, @Res() res: Response,
) { ) {
const userId = req.user!.profile!.id; const userId = req.user!.profile!.id;
const tenantId = req.user.tenantId || req.user.tenant_id || 'default'; const tenantId = req.user!.tenantId || req.user!.tenant_id || 'default';
// Generate report (now persists to storage and database) // Generate report (now persists to storage and database)
const { buffer, metadata, reportId } = await this.reportsService.generateReport(dto, userId, tenantId); const { buffer, metadata, reportId } = await this.reportsService.generateReport(dto, userId, tenantId);

View File

@ -123,15 +123,26 @@ export class BonusCoinsService {
userStats.ml_coins_earned_total += dto.amount; userStats.ml_coins_earned_total += dto.amount;
// 5. Registrar la transacción en metadata (historial) // 5. Registrar la transacción en metadata (historial)
interface BonusHistoryEntry {
teacher_id: string;
amount: number;
reason?: string;
granted_at: string;
previous_balance: number;
new_balance: number;
}
type MetadataWithHistory = { bonus_history?: BonusHistoryEntry[] };
if (!userStats.metadata) { if (!userStats.metadata) {
userStats.metadata = {}; userStats.metadata = {};
} }
if (!userStats.metadata.bonus_history) { const metadataTyped = userStats.metadata as MetadataWithHistory;
userStats.metadata.bonus_history = []; if (!metadataTyped.bonus_history) {
metadataTyped.bonus_history = [];
} }
userStats.metadata.bonus_history.push({ metadataTyped.bonus_history.push({
teacher_id: teacherId, teacher_id: teacherId,
amount: dto.amount, amount: dto.amount,
reason: dto.reason, reason: dto.reason,