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
349 lines
11 KiB
Markdown
349 lines
11 KiB
Markdown
# 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)
|
|
```sql
|
|
-- ========== 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
|
|
```sql
|
|
-- ========== 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
|
|
```sql
|
|
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)
|
|
```sql
|
|
-- 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
|
|
```sql
|
|
-- ========== 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)
|
|
```typescript
|
|
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
|
|
```typescript
|
|
/**
|
|
* 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)
|
|
```typescript
|
|
import { getUserAchievements } from '../api/achievementsAPI';
|
|
```
|
|
|
|
### Código DESPUÉS
|
|
```typescript
|
|
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)
|
|
```typescript
|
|
/**
|
|
* @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
|
|
```bash
|
|
# Backend tests
|
|
cd apps/backend && npm run test -- --testPathPattern=achievements
|
|
|
|
# Frontend tests
|
|
cd apps/frontend && npm run test -- --testPathPattern=achievements
|
|
```
|
|
|
|
### Test Manual E2E
|
|
1. Completar un achievement → Verificar NO recibe XP/Coins automático
|
|
2. Ver notificación de achievement desbloqueado
|
|
3. Click "Reclamar" → Verificar SÍ recibe XP/Coins
|
|
4. 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)
|