Cambios incluidos: - INDICE-DIRECTIVAS-WORKSPACE.yml actualizado - Perfiles de agentes: PERFIL-ML.md, PERFIL-SECURITY.md - Directivas SIMCO actualizadas: - SIMCO-ASIGNACION-PERFILES.md - SIMCO-CCA-SUBAGENTE.md - SIMCO-CONTEXT-ENGINEERING.md - SIMCO-CONTEXT-RESOLUTION.md - SIMCO-DELEGACION-PARALELA.md - Inventarios actualizados: DEVENV-MASTER, DEVENV-PORTS - Documentos de analisis agregados: - Analisis y planes de fix student portal - Analisis scripts BD - Analisis achievements, duplicados, gamification - Auditoria documentacion gamilit - Backlog discrepancias NEXUS - Planes maestros de resolucion - Reportes de ejecucion agregados - Knowledge base gamilit README actualizado - Referencia submodulo gamilit actualizada (commit beb94f7) Validaciones: - Plan validado contra directivas SIMCO-GIT - Dependencias verificadas - Build gamilit: EXITOSO
11 KiB
FASE 5: REFINAMIENTO DEL PLAN - CAMBIOS ESPECÍFICOS
Fecha: 2026-01-10 Proyecto: Gamilit Estado: REFINAMIENTO COMPLETADO
1. RESUMEN DE CAMBIOS
Archivos a Modificar
| # | Archivo | Tipo | Cambio Principal |
|---|---|---|---|
| 1 | 01-trg_achievement_unlocked.sql |
SQL Trigger | Remover distribución rewards |
| 2 | check_and_award_achievements.sql |
SQL Function | Remover distribución rewards |
| 3 | achievements.service.ts |
Backend | Llamar SQL function en claimRewards |
| 4 | achievementsStore.ts |
Frontend | Migrar a gamificationApi |
| 5 | /hooks/useAchievements.ts |
Frontend | Agregar @deprecated |
2. CAMBIO A.1: Trigger fn_on_achievement_unlocked
Archivo: /apps/database/ddl/schemas/gamification_system/triggers/01-trg_achievement_unlocked.sql
Objetivo: Remover distribución de XP y ML Coins (secciones 1 y 2), mantener SOLO notificación (sección 3)
Código ANTES (Líneas 37-104)
-- ========== 1. Otorgar XP (si hay) ==========
IF v_xp_reward > 0 THEN
UPDATE gamification_system.user_stats
SET total_xp = total_xp + v_xp_reward,
updated_at = CURRENT_TIMESTAMP
WHERE user_id = NEW.user_id;
-- ... más código
END IF;
-- ========== 2. Otorgar ML Coins (si hay) ==========
IF v_coins_reward > 0 THEN
-- ... código de distribución de coins
-- ... INSERT INTO ml_coins_transactions
END IF;
Código DESPUÉS
-- ========== 1. REMOVIDO: XP se otorga en claim_achievement_reward ==========
-- Comentario: Modelo Claim-to-Earn - Rewards solo al reclamar
-- IF v_xp_reward > 0 THEN ... END IF;
-- ========== 2. REMOVIDO: ML Coins se otorgan en claim_achievement_reward ==========
-- Comentario: Modelo Claim-to-Earn - Rewards solo al reclamar
-- IF v_coins_reward > 0 THEN ... END IF;
Función Completa Refinada
CREATE OR REPLACE FUNCTION gamification_system.fn_on_achievement_unlocked()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_achievement RECORD;
v_xp_reward INTEGER;
v_coins_reward INTEGER;
v_notification_id UUID;
BEGIN
-- Solo ejecutar cuando se completa un achievement
IF NEW.is_completed = true AND (OLD IS NULL OR OLD.is_completed = false) THEN
-- Obtener datos del achievement
SELECT id, name, description, rewards
INTO v_achievement
FROM gamification_system.achievements
WHERE id = NEW.achievement_id;
IF FOUND THEN
-- Extraer recompensas para mostrar en notificación (NO se otorgan aquí)
v_xp_reward := COALESCE((v_achievement.rewards->>'xp')::INTEGER, 0);
v_coins_reward := COALESCE((v_achievement.rewards->>'ml_coins')::INTEGER, 0);
-- ========== MODELO CLAIM-TO-EARN ==========
-- NOTA: XP y ML Coins NO se otorgan aquí.
-- Se otorgan ÚNICAMENTE al reclamar via claim_achievement_reward()
-- Esto evita triple distribución de rewards
-- ========== Crear Notificación (Sistema Multi-Canal) ==========
INSERT INTO notifications.notifications (
user_id, type, title, message, data, priority, channels, status, metadata
) VALUES (
NEW.user_id,
'achievement',
'🏆 ¡Achievement Desbloqueado!',
format('Has desbloqueado: %s - ¡Reclama tus recompensas!', v_achievement.name),
jsonb_build_object(
'achievement_id', v_achievement.id,
'achievement_name', v_achievement.name,
'xp_reward', v_xp_reward,
'coins_reward', v_coins_reward,
'claim_required', true -- Nuevo campo para indicar que debe reclamar
),
'high',
ARRAY['in_app']::varchar[],
'sent',
jsonb_build_object(
'icon', '🏆',
'action_url', format('/achievements?claim=%s', v_achievement.id),
'related_entity_type', 'achievement',
'related_entity_id', v_achievement.id
)
)
RETURNING id INTO v_notification_id;
-- Marcar notificación como enviada
UPDATE gamification_system.user_achievements
SET notified = true,
metadata = metadata || jsonb_build_object('notification_id', v_notification_id)
WHERE id = NEW.id;
RAISE NOTICE 'Achievement unlocked (pending claim): user_id=%, achievement_id=%, pending_xp=%, pending_coins=%',
NEW.user_id, v_achievement.id, v_xp_reward, v_coins_reward;
END IF;
END IF;
RETURN NEW;
END;
$$;
3. CAMBIO A.2: check_and_award_achievements.sql
Archivo: /apps/database/ddl/schemas/gamification_system/functions/check_and_award_achievements.sql
Objetivo: Remover distribución de XP y ML Coins, mantener solo INSERT en user_achievements
Código ANTES (Líneas 102-132)
-- Obtener balance actual ANTES de actualizar (con row lock)
SELECT ml_coins INTO v_current_balance ...
-- Calcular nuevo balance
v_new_balance := COALESCE(v_current_balance, 0) + COALESCE(v_achievement.ml_coins_reward, 0);
-- Actualizar estadisticas del usuario
UPDATE gamification_system.user_stats
SET
total_xp = COALESCE(total_xp, 0) + v_xp_reward,
ml_coins = v_new_balance,
achievements_earned = COALESCE(achievements_earned, 0) + 1,
updated_at = NOW()
WHERE user_id = p_user_id;
-- Registrar transaccion de coins
IF COALESCE(v_achievement.ml_coins_reward, 0) > 0 THEN
INSERT INTO gamification_system.ml_coins_transactions (...) ...
END IF;
Código DESPUÉS
-- ========== MODELO CLAIM-TO-EARN ==========
-- NOTA: XP y ML Coins NO se otorgan aquí.
-- Se otorgan ÚNICAMENTE al reclamar via claim_achievement_reward()
-- Solo incrementar contador de achievements earned
UPDATE gamification_system.user_stats
SET
achievements_earned = COALESCE(achievements_earned, 0) + 1,
updated_at = NOW()
WHERE user_id = p_user_id;
-- NO registrar transaccion de coins aquí - se hace en claim_achievement_reward
4. CAMBIO B.1: achievements.service.ts
Archivo: /apps/backend/src/modules/gamification/services/achievements.service.ts
Objetivo: claimRewards() debe llamar función SQL y retornar rewards
Código ANTES (Líneas 745-759)
async claimRewards(userId: string, achievementId: string): Promise<UserAchievement> {
const userAchievement = await this.checkProgress(userId, achievementId);
if (!userAchievement.is_completed) {
throw new BadRequestException(`Achievement ${achievementId} is not completed yet`);
}
if (userAchievement.rewards_claimed) {
throw new BadRequestException(`Rewards already claimed for achievement ${achievementId}`);
}
userAchievement.rewards_claimed = true;
return this.userAchievementRepo.save(userAchievement);
}
Código DESPUÉS
/**
* Reclama las recompensas de un achievement completado
* Usa la función SQL claim_achievement_reward para distribución atómica
*
* @param userId - ID del usuario
* @param achievementId - ID del achievement a reclamar
* @returns UserAchievement actualizado con xp_granted y coins_granted
*/
async claimRewards(userId: string, achievementId: string): Promise<{
userAchievement: UserAchievement;
xp_granted: number;
coins_granted: number;
}> {
// Llamar función SQL que:
// 1. Valida que el achievement esté completado
// 2. Valida que no se haya reclamado antes
// 3. Actualiza rewards_claimed = true
// 4. Distribuye XP y ML Coins
// 5. Registra transacción de coins
const result = await this.dataSource.query(
`SELECT * FROM gamification_system.claim_achievement_reward($1, $2)`,
[userId, achievementId]
);
const claimResult = result[0];
if (!claimResult.success) {
throw new BadRequestException(claimResult.message);
}
// Obtener userAchievement actualizado
const userAchievement = await this.checkProgress(userId, achievementId);
this.logger.log(
`Achievement ${achievementId} rewards claimed for user ${userId}: ` +
`XP=${claimResult.xp_granted}, Coins=${claimResult.coins_granted}`
);
return {
userAchievement,
xp_granted: claimResult.xp_granted,
coins_granted: claimResult.coins_granted,
};
}
5. CAMBIO C.1: achievementsStore.ts
Archivo: /apps/frontend/src/features/gamification/social/store/achievementsStore.ts
Objetivo: Migrar de achievementsAPI a gamificationApi
Código ANTES (Línea 16)
import { getUserAchievements } from '../api/achievementsAPI';
Código DESPUÉS
import { gamificationApi } from '@/lib/api/gamification.api';
// En fetchAchievements (línea 162):
// ANTES:
const achievementsWithProgress = await getUserAchievements(userId);
// DESPUÉS:
const achievementsWithProgress = await gamificationApi.getUserAchievements(userId);
6. CAMBIO C.2: /hooks/useAchievements.ts (Deprecate)
Archivo: /apps/frontend/src/hooks/useAchievements.ts
Objetivo: Agregar notice de deprecación
Código a Agregar (Líneas 1-20)
/**
* @deprecated Este hook está DEPRECADO desde 2026-01-10
*
* RAZONES:
* 1. Contiene 450+ líneas de achievement definitions hardcodeadas
* 2. Las recompensas pueden no coincidir con la base de datos
* 3. La detección de achievements se hace en el backend (detectAndGrantEarned)
*
* USA EN SU LUGAR:
* - useAchievements de '@/features/gamification/social/hooks/useAchievements'
* - gamificationApi de '@/lib/api/gamification.api' para API calls
*
* @see ANALISIS-DUPLICADOS-ACHIEVEMENTS-2026-01-10.md para detalles
*/
console.warn(
'[DEPRECATED] useAchievements from /hooks is deprecated. ' +
'Use useAchievements from @/features/gamification/social/hooks instead.'
);
7. ORDEN DE EJECUCIÓN FINAL
FASE A: SQL (Detener triple distribución)
├── A.1: Modificar fn_on_achievement_unlocked (solo notificación)
└── A.2: Modificar check_and_award_achievements (solo contador)
FASE B: Backend (Habilitar claim-to-earn)
└── B.1: Modificar achievements.service.ts claimRewards()
FASE C: Frontend (Cleanup)
├── C.1: Modificar achievementsStore.ts imports
└── C.2: Deprecar /hooks/useAchievements.ts
8. VALIDACIÓN POST-EJECUCIÓN
Tests a Ejecutar
# Backend tests
cd apps/backend && npm run test -- --testPathPattern=achievements
# Frontend tests
cd apps/frontend && npm run test -- --testPathPattern=achievements
Test Manual E2E
- Completar un achievement → Verificar NO recibe XP/Coins automático
- Ver notificación de achievement desbloqueado
- Click "Reclamar" → Verificar SÍ recibe XP/Coins
- Intentar reclamar de nuevo → Verificar error "Ya reclamado"
Refinado por: Claude (Arquitecto Técnico) Fecha: 2026-01-10 Estado: FASE 5 COMPLETADA - Listo para FASE 6 (Ejecución)