workspace-v1/orchestration/analisis/REFINAMIENTO-PLAN-DUPLICADOS-2026-01-10.md
rckrdmrd e56e927a4d [MAINT-001] docs(orchestration): Actualizacion directivas SIMCO, perfiles y documentacion
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
2026-01-10 04:51:28 -06:00

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)