- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
42 KiB
MATRIZ DE DEPENDENCIAS BIDIRECCIONALES
Student Portal - Correcciones P0
Fecha: 2025-11-24 Última actualización: 2025-11-24 (Post-GAP-008) Alcance: GAP-001, GAP-006, GAP-007, GAP-008 Tipo: Análisis de dependencias bidireccionales (consume + es consumido) Estado: ✅ COMPLETADO
📋 INTRODUCCIÓN
Este documento mapea TODAS las dependencias bidireccionales entre los componentes implementados en las correcciones P0. Cada entrada muestra:
CONSUME (Dependencias salientes):
- Objetos/servicios/librerías que este componente necesita para funcionar
ES CONSUMIDO POR (Dependencias entrantes):
- Objetos/componentes que dependen de este componente
Formato:
[COMPONENTE A]
├─ CONSUME → [COMPONENTE B, C, D]
└─ ES CONSUMIDO POR → [COMPONENTE E, F]
🗂️ ÍNDICE DE COMPONENTES
Backend (4 archivos)
- MissionsService
- MissionsController
- AuthService.getUserStatistics() ✨ NUEVO (GAP-008)
- UsersController ✨ NUEVO (GAP-008)
Frontend (4 archivos)
Base de Datos (10 tablas)
- gamification.missions
- gamification_system.user_stats
- gamification.user_ranks
- gamification.user_achievements ✨ NUEVO (GAP-008)
- gamification.achievements ✨ NUEVO (GAP-008)
- economy.ml_coins_transactions
- progress_tracking.exercise_submissions ✨ NUEVO (GAP-008)
Servicios Externos (4 packages)
🔗 MATRIZ COMPLETA DE DEPENDENCIAS
1. MissionsService
Archivo: apps/backend/src/modules/gamification/services/missions.service.ts
Gap: STUDENT-GAP-001
Tipo: NestJS Service (Backend)
CONSUME (Dependencias Salientes)
| # | Componente | Tipo | Método/Propiedad | Propósito | Impacto si Falla |
|---|---|---|---|---|---|
| 1.1 | Mission Entity | TypeORM Repository | findOne(), save() |
Consultar y actualizar misiones en BD | 🔴 Crítico - No funciona |
| 1.2 | MLCoinsService | NestJS Service | addCoins(userId, amount, reason, metadata) |
Otorgar ML Coins al student | 🔴 Crítico - Coins no se otorgan |
| 1.3 | UserStatsService | NestJS Service | addXp(userId, amount) |
Otorgar XP al student | 🔴 Crítico - XP no se otorga |
| 1.4 | RanksService | NestJS Service | getCurrentRank(userId) |
Obtener rango actual para detectar promoción | 🟡 Medio - No detecta promoción |
Detalles de Dependencias:
1.1 Mission Entity (TypeORM Repository)
// Inyección en constructor
constructor(
@InjectRepository(Mission)
private readonly missionsRepository: Repository<Mission>,
) {}
// Uso en claimRewards()
const mission = await this.missionsRepository.findOne({
where: { id: missionId, user_id: userId, status: 'completed' },
});
mission.status = 'claimed';
mission.claimed_at = new Date();
await this.missionsRepository.save(mission);
- Tabla BD:
gamification.missions - Operaciones: SELECT (findOne), UPDATE (save)
- Impacto: Si falla, método lanza exception
1.2 MLCoinsService
// Inyección en constructor
private readonly mlCoinsService: MLCoinsService
// Uso en claimRewards()
await this.mlCoinsService.addCoins(
userId,
ml_coins_reward,
`Recompensa por completar misión: ${mission.title}`,
{
mission_id: missionId,
mission_type: mission.type,
mission_title: mission.title,
}
);
- Módulo:
economy/services/ml-coins.service.ts - Tabla BD afectada:
economy.ml_coins_transactions - Impacto: Si falla, misión se reclamaría pero coins NO se otorgarían (inconsistencia)
1.3 UserStatsService
// Inyección en constructor
private readonly userStatsService: UserStatsService
// Uso en claimRewards()
await this.userStatsService.addXp(userId, xp_reward);
// NOTA: Este método activa trigger check_user_promotion_on_xp_update
- Módulo:
users/services/user-stats.service.ts - Tabla BD afectada:
users.user_stats(total_xp) - Trigger activado:
check_user_promotion_on_xp_update - Impacto: Si falla, XP no se otorga (inconsistencia crítica)
1.4 RanksService
// Inyección en constructor
private readonly ranksService: RanksService
// Uso en claimRewards() (llamado 2 veces)
const previousRank = await this.ranksService.getCurrentRank(userId); // Antes de otorgar XP
// ... otorgar XP ...
const newRank = await this.ranksService.getCurrentRank(userId); // Después de otorgar XP
const rankPromotion = previousRank.rank !== newRank.rank;
- Módulo:
gamification/services/ranks.service.ts - Tabla BD consultada:
gamification.user_ranks - Impacto: Si falla, promoción NO se detecta pero XP se otorga correctamente
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Tipo | Método que Consume | Propósito | Flujo |
|---|---|---|---|---|---|
| 1.5 | MissionsController | NestJS Controller | claimMissionRewards(@Param('id')) |
Exponer endpoint HTTP POST /missions/:id/claim | Controller → Service → BD |
| 1.6 | Frontend: useMissions Hook | React Query | useClaimMissionRewards() mutation |
Permitir al student reclamar misión desde UI | UI → HTTP → Controller → Service |
Detalles de Consumidores:
1.5 MissionsController
// apps/backend/src/modules/gamification/controllers/missions.controller.ts
@Post(':id/claim')
@UseGuards(JwtAuthGuard, RolesGuard)
async claimMissionRewards(
@Param('id') missionId: string,
@Req() req: Request,
) {
const userId = req.user.id;
return this.missionsService.claimRewards(missionId, userId);
}
- Endpoint:
POST /missions/:id/claim - Autenticación: JWT required
- Autorización: Solo el owner puede reclamar
1.6 Frontend: useMissions Hook
// apps/frontend/src/apps/student/hooks/useMissions.ts
const useClaimMissionRewards = () => {
return useMutation({
mutationFn: (missionId: string) =>
apiClient.post(`/missions/${missionId}/claim`),
onSuccess: (data) => {
// Invalidar caché de misiones
queryClient.invalidateQueries(['missions']);
// Mostrar toast con recompensas
toast.success(`Recompensas reclamadas: +${data.rewards_granted.xp_awarded} XP`);
},
});
};
- Componente que lo usa:
MissionsPage.tsx - Acción del usuario: Hacer clic en botón "Reclamar"
2. MissionsController
Archivo: apps/backend/src/modules/gamification/controllers/missions.controller.ts
Gap: STUDENT-GAP-001
Tipo: NestJS Controller (Backend)
CONSUME (Dependencias Salientes)
| # | Componente | Tipo | Método/Propiedad | Propósito | Impacto si Falla |
|---|---|---|---|---|---|
| 2.1 | MissionsService | NestJS Service | claimRewards(missionId, userId) |
Delegar lógica de negocio | 🔴 Crítico - Endpoint no funciona |
| 2.2 | JwtAuthGuard | NestJS Guard | Validate JWT token | Autenticación | 🔴 Crítico - Endpoint desprotegido |
| 2.3 | RolesGuard | NestJS Guard | Validate user role | Autorización | 🟡 Medio - Sin control de roles |
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Tipo | Endpoint | Propósito | Flujo |
|---|---|---|---|---|---|
| 2.4 | Frontend: apiClient | Axios | POST /missions/:id/claim |
Cliente HTTP desde frontend | Browser → API Gateway → Controller |
| 2.5 | API Gateway | NestJS | Global prefix /api |
Routing HTTP | Internet → Gateway → Controller |
3. AuthService.getUserStatistics()
Archivo: apps/backend/src/modules/auth/services/auth.service.ts
Gap: STUDENT-GAP-008
Tipo: NestJS Service Method (Backend)
CONSUME (Dependencias Salientes)
| # | Componente | Tipo | Método/Propiedad | Propósito | Impacto si Falla |
|---|---|---|---|---|---|
| 3.1 | UserStats Entity | TypeORM Repository | findOne() |
Consultar XP, modules, streak del usuario | 🔴 Crítico - Stats incorrectos |
| 3.2 | UserRank Entity | TypeORM Repository | findOne({ is_current: true }) |
Consultar rank actual del usuario | 🔴 Crítico - Rank incorrecto |
| 3.3 | UserAchievement Entity | TypeORM Repository | count({ is_completed: true }) |
Contar logros completados | 🟡 Medio - Stats incompletos |
| 3.4 | Achievement Entity | TypeORM Repository | count({ is_active: true }) |
Contar logros totales disponibles | 🟡 Medio - Stats incompletos |
| 3.5 | MLCoinsTransaction Entity | TypeORM Repository | createQueryBuilder().select(SUM) |
Calcular balance de ML Coins | 🔴 Crítico - Balance incorrecto |
| 3.6 | ExerciseSubmission Entity | TypeORM Repository | count({ is_correct: true }) |
Contar ejercicios completados | 🟡 Medio - Stats incompletos |
Detalles de Dependencias:
3.1-3.6 TypeORM Repositories (Multi-Schema)
// Inyección en constructor (6 repositorios)
constructor(
@InjectRepository(UserStats, 'gamification')
private readonly userStatsRepository: Repository<UserStats>,
@InjectRepository(UserRank, 'gamification')
private readonly userRanksRepository: Repository<UserRank>,
@InjectRepository(UserAchievement, 'gamification')
private readonly userAchievementsRepository: Repository<UserAchievement>,
@InjectRepository(Achievement, 'gamification')
private readonly achievementsRepository: Repository<Achievement>,
@InjectRepository(MLCoinsTransaction, 'gamification')
private readonly mlCoinsTransactionsRepository: Repository<MLCoinsTransaction>,
@InjectRepository(ExerciseSubmission, 'progress')
private readonly exerciseSubmissionsRepository: Repository<ExerciseSubmission>,
) {}
// Query 1: ML Coins Balance (SUM aggregation)
const mlCoinsResult = await this.mlCoinsTransactionsRepository
.createQueryBuilder('transaction')
.select('COALESCE(SUM(transaction.amount), 0)', 'ml_coins')
.where('transaction.user_id = :userId', { userId })
.getRawOne();
// Query 2: User Stats
const userStats = await this.userStatsRepository.findOne({
where: { user_id: userId },
});
// Query 3: Current Rank (filter by is_current=true)
const userRank = await this.userRanksRepository.findOne({
where: { user_id: userId, is_current: true },
});
// Query 4-6: Counts
const achievementsEarned = await this.userAchievementsRepository.count({
where: { user_id: userId, is_completed: true },
});
- Multi-Schema Integration:
- 'gamification' connection: user_stats, user_ranks, achievements, user_achievements, ml_coins_transactions
- 'progress' connection: exercise_submissions
- Edge Cases Manejados:
- Usuario sin transacciones: COALESCE retorna 0
- Usuario sin stats: operador
||fallback a 0 - Usuario sin rank: fallback a 'Ajaw' (rank inicial)
- Todos los counts retornan 0 si no hay datos
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Tipo | Método que Consume | Propósito | Flujo |
|---|---|---|---|---|---|
| 3.7 | UsersController | NestJS Controller | getUserStatistics(@Param('id')) |
Exponer endpoint HTTP GET /users/:id/statistics | Controller → Service → BD (6 queries) |
| 3.8 | Frontend: useUserStatistics Hook | React Query | useQuery() |
Fetch de estadísticas desde frontend | UI → HTTP → Controller → Service |
4. UsersController
Archivo: apps/backend/src/modules/auth/controllers/users.controller.ts
Gap: STUDENT-GAP-008
Tipo: NestJS Controller (Backend)
CONSUME (Dependencias Salientes)
| # | Componente | Tipo | Método/Propiedad | Propósito | Impacto si Falla |
|---|---|---|---|---|---|
| 4.1 | AuthService | NestJS Service | getUserStatistics(userId) |
Delegar lógica de negocio | 🔴 Crítico - Endpoint no funciona |
| 4.2 | JwtAuthGuard | NestJS Guard | Validate JWT token | Autenticación | 🔴 Crítico - Endpoint desprotegido |
Detalles:
@Get(':id/statistics')
@UseGuards(JwtAuthGuard)
async getUserStatistics(@Param('id') userId: string) {
return this.authService.getUserStatistics(userId);
}
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Tipo | Endpoint | Propósito | Flujo |
|---|---|---|---|---|---|
| 4.3 | Frontend: apiClient | Axios | GET /users/:id/statistics |
Cliente HTTP desde frontend | Browser → API → Controller |
| 4.4 | Frontend: useUserStatistics Hook | React Query | useQuery() |
Fetch con caché de 2 minutos | Hook → apiClient → Endpoint |
5. useUserStatistics Hook
Archivo: apps/frontend/src/shared/hooks/useUserStatistics.ts
Gap: STUDENT-GAP-006
Tipo: React Custom Hook (Frontend)
CONSUME (Dependencias Salientes)
| # | Componente | Tipo | Método/Propiedad | Propósito | Impacto si Falla |
|---|---|---|---|---|---|
| 3.1 | React Query | Library | useQuery() |
Gestión de estado del servidor con caché | 🔴 Crítico - Hook no compila |
| 3.2 | apiClient | Axios instance | get('/users/:userId/statistics') |
Fetch de estadísticas desde backend | 🔴 Crítico - No carga datos |
| 3.3 | Backend: GET /users/:userId/statistics | REST API | Endpoint HTTP | Obtener stats del usuario | 🔴 Crítico - Hook devuelve error |
Detalles de Dependencias:
3.1 React Query
import { useQuery } from '@tanstack/react-query';
export function useUserStatistics(userId: string | undefined) {
return useQuery<UserStatistics>({
queryKey: ['userStatistics', userId],
queryFn: async () => {
if (!userId) throw new Error('User ID is required');
const response = await apiClient.get(`/users/${userId}/statistics`);
return response.data;
},
enabled: !!userId,
staleTime: 2 * 60 * 1000, // 2 minutos
refetchOnWindowFocus: true,
});
}
- Funcionalidades usadas:
queryKey- Caché identificationqueryFn- Async function para fetchenabled- Conditional fetchingstaleTime- Tiempo antes de considerar datos obsoletosrefetchOnWindowFocus- Refetch automático al volver a la ventana
- Return:
{ data, isLoading, error, refetch }
3.2 apiClient
import { apiClient } from '../services/api/apiClient';
const response = await apiClient.get(`/users/${userId}/statistics`);
- Archivo:
apps/frontend/src/services/api/apiClient.ts - Interceptors aplicados:
- Request: Agregar JWT token automáticamente
- Response: Manejar errores 401 (redirect a login)
- Headers enviados:
Authorization: Bearer <JWT>
3.3 Backend Endpoint
- URL:
GET /api/users/:userId/statistics - Controller:
UsersController.getUserStatistics() - Service:
AuthService.getUserStatistics() - Response esperado:
{
ml_coins: number,
achievements_unlocked: number,
achievements_available: number,
total_xp: number,
current_rank: { rank: string, icon: string, color: string },
exercises_completed: number
}
- NOTA: Backend actualmente devuelve valores "0" (mock) - Ver GAP-008
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Tipo | Uso | Propósito | Frecuencia |
|---|---|---|---|---|---|
| 3.4 | ProfilePage Component | React Component | const { data, isLoading, error } = useUserStatistics(user?.id) |
Mostrar stats dinámicos en perfil | 1x por mount + refetch on focus |
Detalles de Consumidores:
3.4 ProfilePage Component
// apps/frontend/src/apps/student/pages/ProfilePage.tsx
export default function ProfilePage() {
const { user } = useAuth();
const { data: userStats, isLoading, error } = useUserStatistics(user?.id);
if (isLoading) return <Loader2 />;
if (error) return <ErrorMessage />;
const stats = [
{ label: 'ML Coins', value: userStats.ml_coins.toString() },
{ label: 'Logros', value: `${userStats.achievements_unlocked}/${userStats.achievements_available}` },
// ...
];
return <div>{/* Renderizar stats */}</div>;
}
4. ProfilePage Component
Archivo: apps/frontend/src/apps/student/pages/ProfilePage.tsx
Gap: STUDENT-GAP-006
Tipo: React Component (Frontend)
CONSUME (Dependencias Salientes)
| # | Componente | Tipo | Método/Propiedad | Propósito | Impacto si Falla |
|---|---|---|---|---|---|
| 4.1 | useUserStatistics Hook | Custom Hook | useUserStatistics(userId) |
Fetch de estadísticas desde API | 🔴 Crítico - Stats no cargan |
| 4.2 | useAuth Hook | Context Hook | user.id, logout() |
Obtener usuario autenticado | 🔴 Crítico - No sabe qué user mostrar |
| 4.3 | Lucide Icons | Library | Loader2, Coins, Trophy, Zap, Crown |
Íconos de UI | 🟡 Medio - No compila |
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Tipo | Ruta | Propósito | Trigger |
|---|---|---|---|---|---|
| 4.4 | React Router | Routing | /student/profile |
Renderizar página de perfil | Usuario navega a URL |
5. profileAPI Service
Archivo: apps/frontend/src/services/api/profileAPI.ts
Gap: STUDENT-GAP-007
Tipo: API Service (Frontend)
CONSUME (Dependencias Salientes)
| # | Componente | Tipo | Endpoint | Propósito | Impacto si Falla |
|---|---|---|---|---|---|
| 5.1 | apiClient | Axios instance | put(), post() |
Cliente HTTP con JWT | 🔴 Crítico - Requests no funcionan |
| 5.2 | Backend: PUT /users/:id/profile | REST API | Actualizar perfil | Persistir cambios de perfil | 🔴 Crítico - No guarda |
| 5.3 | Backend: PUT /users/:id/preferences | REST API | Actualizar preferencias | Persistir configuraciones | 🔴 Crítico - No guarda |
| 5.4 | Backend: POST /users/:id/avatar | REST API | Subir avatar | Persistir nueva imagen | 🔴 Crítico - No guarda |
| 5.5 | Backend: PUT /users/:id/password | REST API | Cambiar contraseña | Persistir nueva contraseña | 🔴 Crítico - No guarda |
Detalles de Dependencias:
5.1 apiClient
import { apiClient } from './apiClient';
export const profileAPI = {
updateProfile: async (userId: string, data: UpdateProfileDto) => {
const response = await apiClient.put(`/users/${userId}/profile`, data);
return response.data;
},
// ... otros métodos
};
5.2 Backend: PUT /users/:id/profile
// Request
PUT /api/users/user-123/profile
Headers: { Authorization: 'Bearer <JWT>', Content-Type: 'application/json' }
Body: { first_name: 'Juan', last_name: 'Pérez', email: 'juan@example.com' }
// Response esperado
200 OK
Body: { id: 'user-123', first_name: 'Juan', last_name: 'Pérez', email: 'juan@example.com', ... }
- NOTA: Backend NO implementado completamente (no persiste en BD) - Ver GAP-008
5.3 Backend: PUT /users/:id/preferences
// Request
PUT /api/users/user-123/preferences
Body: {
preferences: {
notifications: { email: true, push: false, in_app: true },
language: 'es',
theme: 'dark'
}
}
// Response esperado
200 OK
Body: { id: 'user-123', preferences: { ... }, ... }
5.4 Backend: POST /users/:id/avatar
// Request
POST /api/users/user-123/avatar
Headers: { Content-Type: 'multipart/form-data' }
Body: FormData { avatar: <File> }
// Response esperado
200 OK
Body: { avatar_url: 'https://storage.gamilit.com/avatars/user-123-timestamp.webp' }
- NOTA: Backend NO implementado (devuelve 501 Not Implemented) - Ver GAP-008
5.5 Backend: PUT /users/:id/password
// Request
PUT /api/users/user-123/password
Body: { current_password: 'oldPass123', new_password: 'newPass456' }
// Response esperado
200 OK (sin body)
// Errores posibles
400 Bad Request: "La contraseña actual es incorrecta"
400 Bad Request: "La nueva contraseña debe tener al menos 8 caracteres"
- NOTA: Backend NO implementado (devuelve 501 Not Implemented) - Ver GAP-008
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Tipo | Métodos Usados | Propósito | Frecuencia |
|---|---|---|---|---|---|
| 5.6 | SettingsPage Component | React Component | updateProfile(), updatePreferences(), uploadAvatar(), updatePassword() |
Persistir cambios de settings | On-demand (clic en botones) |
Detalles de Consumidores:
5.6 SettingsPage Component
// apps/frontend/src/apps/student/pages/SettingsPage.tsx
// Método 1: handleSave() usa updateProfile + updatePreferences
const handleSave = async () => {
await profileAPI.updateProfile(user!.id, profileData);
await profileAPI.updatePreferences(user!.id, preferencesData);
};
// Método 2: handleAvatarUpload() usa uploadAvatar
const handleAvatarUpload = async (file: File) => {
const result = await profileAPI.uploadAvatar(user!.id, file);
setProfile(prev => ({ ...prev, avatar: result.avatar_url }));
};
// Método 3: handlePasswordChange() usa updatePassword
const handlePasswordChange = async () => {
await profileAPI.updatePassword(user!.id, passwordsData);
};
6. SettingsPage Component
Archivo: apps/frontend/src/apps/student/pages/SettingsPage.tsx
Gap: STUDENT-GAP-007
Tipo: React Component (Frontend)
CONSUME (Dependencias Salientes)
| # | Componente | Tipo | Método/Propiedad | Propósito | Impacto si Falla |
|---|---|---|---|---|---|
| 6.1 | profileAPI Service | API Service | updateProfile(), updatePreferences(), uploadAvatar(), updatePassword() |
Persistir cambios en backend | 🔴 Crítico - No guarda cambios |
| 6.2 | useAuth Hook | Context Hook | user.id |
Obtener ID del usuario autenticado | 🔴 Crítico - No sabe qué user editar |
| 6.3 | react-hot-toast | Library | toast.success(), toast.error() |
Mostrar notificaciones | 🟡 Medio - No compila (pero UX pobre) |
| 6.4 | Lucide Icons | Library | Loader2, User, Lock, Bell, Globe, Palette |
Íconos de UI y spinner | 🟡 Medio - No compila |
Detalles de Dependencias:
6.1 profileAPI Service
import { profileAPI } from '@/services/api/profileAPI';
// 1. Guardar perfil y preferencias
const handleSave = async () => {
try {
await profileAPI.updateProfile(user!.id, profileData);
await profileAPI.updatePreferences(user!.id, preferencesData);
toast.success('Configuración guardada correctamente');
} catch (error) {
toast.error('Error al guardar');
}
};
// 2. Subir avatar
const handleAvatarUpload = async (file: File) => {
try {
const result = await profileAPI.uploadAvatar(user!.id, file);
setProfile(prev => ({ ...prev, avatar: result.avatar_url }));
toast.success('Avatar actualizado');
} catch (error) {
toast.error('Error al subir avatar');
}
};
// 3. Cambiar contraseña
const handlePasswordChange = async () => {
try {
await profileAPI.updatePassword(user!.id, passwordsData);
toast.success('Contraseña actualizada');
} catch (error) {
toast.error('Error al cambiar contraseña');
}
};
6.2 useAuth Hook
import { useAuth } from '@/contexts/AuthContext';
export default function SettingsPage() {
const { user, logout } = useAuth();
// user.id se usa en todos los métodos:
await profileAPI.updateProfile(user!.id, ...);
await profileAPI.uploadAvatar(user!.id, ...);
}
6.3 react-hot-toast
import toast from 'react-hot-toast';
// Éxito
toast.success('Configuración guardada correctamente');
// Error
toast.error(error.response?.data?.message || 'Error al guardar');
- Toast duration: 3-5 segundos por defecto
- Position: top-right (configurado en App.tsx con
<Toaster />)
6.4 Lucide Icons
import { Loader2, User, Lock, Bell, Globe, Palette } from 'lucide-react';
// Loading spinner
<Loader2 className="h-4 w-4 animate-spin" />
// Íconos de secciones
<User className="h-5 w-5" /> // Perfil
<Lock className="h-5 w-5" /> // Contraseña
<Bell className="h-5 w-5" /> // Notificaciones
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Tipo | Ruta | Propósito | Trigger |
|---|---|---|---|---|---|
| 6.5 | React Router | Routing | /student/settings |
Renderizar página de settings | Usuario navega a URL |
7. gamification.missions (Tabla)
Tipo: PostgreSQL Table Gap: STUDENT-GAP-001 Schema: gamification
ES CONSUMIDO POR (Dependencias Entrantes - Escritura)
| # | Componente | Operación | Campos Modificados | Propósito | Frecuencia |
|---|---|---|---|---|---|
| 7.1 | MissionsService.claimRewards() | UPDATE | status, claimed_at |
Marcar misión como reclamada | On-demand (student reclama) |
Detalles:
7.1 MissionsService UPDATE
-- Operación realizada por TypeORM
UPDATE gamification.missions
SET status = 'claimed',
claimed_at = NOW()
WHERE id = $1 AND user_id = $2;
- Triggered by: Student hace clic en "Reclamar" en MissionsPage
- Validación previa: Mission status = 'completed' AND claimed_at IS NULL
ES CONSUMIDO POR (Dependencias Entrantes - Lectura)
| # | Componente | Operación | Filtros | Propósito | Frecuencia |
|---|---|---|---|---|---|
| 7.2 | MissionsService.claimRewards() | SELECT | id, user_id, status='completed' |
Validar que misión existe y está completada | On-demand |
| 7.3 | MissionsService.getUserMissions() | SELECT | user_id, status IN (...) |
Listar misiones del student | Multiple veces (mount, refetch) |
8. users.user_stats (Tabla)
Tipo: PostgreSQL Table Gap: STUDENT-GAP-001, STUDENT-GAP-006 Schema: users
ES CONSUMIDO POR (Dependencias Entrantes - Escritura)
| # | Componente | Operación | Campos Modificados | Propósito | Frecuencia |
|---|---|---|---|---|---|
| 8.1 | UserStatsService.addXp() | UPDATE | total_xp |
Incrementar XP del student | On-demand (misión, ejercicio) |
Detalles:
8.1 UserStatsService UPDATE
-- Operación realizada por TypeORM
UPDATE users.user_stats
SET total_xp = total_xp + $1
WHERE user_id = $2;
-- Trigger automático después del UPDATE
EXECUTE FUNCTION gamification.check_user_promotion();
- Trigger:
check_user_promotion_on_xp_updatese ejecuta AFTER UPDATE - Efecto: Actualiza
gamification.user_rankssi XP cruza umbral
ES CONSUMIDO POR (Dependencias Entrantes - Lectura)
| # | Componente | Operación | Filtros | Propósito | Frecuencia |
|---|---|---|---|---|---|
| 8.2 | AuthService.getUserStatistics() | SELECT | user_id |
Obtener total_xp para stats | On-demand (ProfilePage mount) |
9. gamification.user_ranks (Tabla)
Tipo: PostgreSQL Table Gap: STUDENT-GAP-001, STUDENT-GAP-006 Schema: gamification
ES CONSUMIDO POR (Dependencias Entrantes - Escritura)
| # | Componente | Operación | Campos Modificados | Propósito | Trigger |
|---|---|---|---|---|---|
| 9.1 | check_user_promotion() Function | UPDATE | rank, promoted_at |
Promocionar student a nuevo rango | Trigger automático (XP update) |
Detalles:
9.1 check_user_promotion() Function
-- Función PL/pgSQL ejecutada por trigger
CREATE OR REPLACE FUNCTION gamification.check_user_promotion()
RETURNS TRIGGER AS $$
DECLARE
current_xp INTEGER;
new_rank VARCHAR;
BEGIN
-- Obtener XP actual del student
SELECT total_xp INTO current_xp
FROM users.user_stats
WHERE user_id = NEW.user_id;
-- Determinar nuevo rango según XP
new_rank := CASE
WHEN current_xp >= 2000 THEN 'K''uk''ulkan'
WHEN current_xp >= 1000 THEN 'Halach Uinic'
WHEN current_xp >= 500 THEN 'Ah K''in'
WHEN current_xp >= 200 THEN 'Nacom'
ELSE 'Ajaw'
END;
-- Actualizar rango si cambió
UPDATE gamification.user_ranks
SET rank = new_rank,
promoted_at = NOW()
WHERE user_id = NEW.user_id
AND rank != new_rank;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
- Triggered by: UPDATE en
users.user_stats.total_xp - Frecuencia: Cada vez que un student gana XP (misión, ejercicio)
ES CONSUMIDO POR (Dependencias Entrantes - Lectura)
| # | Componente | Operación | Filtros | Propósito | Frecuencia |
|---|---|---|---|---|---|
| 9.2 | RanksService.getCurrentRank() | SELECT | user_id |
Obtener rango actual del student | Multiple (antes/después de XP, ProfilePage) |
| 9.3 | AuthService.getUserStatistics() | SELECT | user_id |
Incluir rango en stats | On-demand (ProfilePage mount) |
10. economy.ml_coins_transactions (Tabla)
Tipo: PostgreSQL Table Gap: STUDENT-GAP-001 Schema: economy
ES CONSUMIDO POR (Dependencias Entrantes - Escritura)
| # | Componente | Operación | Campos Insertados | Propósito | Frecuencia |
|---|---|---|---|---|---|
| 10.1 | MLCoinsService.addCoins() | INSERT | user_id, amount, reason, metadata, created_at |
Registrar transacción de coins | On-demand (misión, ejercicio) |
Detalles:
10.1 MLCoinsService INSERT
-- Operación realizada por TypeORM
INSERT INTO economy.ml_coins_transactions
(user_id, amount, reason, metadata, created_at)
VALUES
($1, $2, $3, $4, NOW());
-- Ejemplo de transacción al reclamar misión
-- user_id: 'user-123'
-- amount: 50
-- reason: 'Recompensa por completar misión: Completa 5 ejercicios'
-- metadata: { mission_id: 'mission-456', mission_type: 'daily', mission_title: 'Completa 5 ejercicios' }
- Registro de auditoría: Todas las transacciones quedan registradas (no se eliminan)
- Saldo calculado:
SELECT SUM(amount) FROM ml_coins_transactions WHERE user_id = $1
11. React Query (@tanstack/react-query)
Tipo: External Library (npm package) Gap: STUDENT-GAP-006 Versión: ^4.x o ^5.x
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Hook/Método Usado | Propósito | Configuración |
|---|---|---|---|---|
| 11.1 | useUserStatistics Hook | useQuery() |
Fetch de estadísticas con caché | staleTime: 120000, refetchOnWindowFocus: true |
| 11.2 | useMissions Hook | useQuery(), useMutation() |
Fetch de misiones + mutación de claim | N/A (no modificado en este sprint) |
12. react-hot-toast
Tipo: External Library (npm package) Gap: STUDENT-GAP-007 Versión: ^2.x
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Métodos Usados | Propósito | Tipos de Toast |
|---|---|---|---|---|
| 12.1 | SettingsPage Component | toast.success(), toast.error() |
Notificaciones de operaciones | Success: guardado exitoso, Error: fallo al guardar |
Detalles:
12.1 SettingsPage Toast Usage
// Éxito
toast.success('Configuración guardada correctamente');
toast.success('Avatar actualizado correctamente');
toast.success('Contraseña actualizada correctamente');
// Error
toast.error(error.response?.data?.message || 'Error al guardar la configuración');
toast.error('La imagen no puede superar los 2MB');
toast.error('Formato inválido. Usa JPG, PNG o WebP');
13. apiClient (Axios Instance)
Tipo: Internal Service
Archivo: apps/frontend/src/services/api/apiClient.ts
Gap: STUDENT-GAP-006, STUDENT-GAP-007
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Métodos Usados | Endpoints Llamados | Propósito |
|---|---|---|---|---|
| 13.1 | useUserStatistics Hook | get() |
GET /users/:id/statistics |
Fetch de stats |
| 13.2 | profileAPI Service | put(), post() |
PUT /users/:id/profile, PUT /users/:id/preferences, POST /users/:id/avatar, PUT /users/:id/password |
Operaciones de settings |
Características:
- Base URL:
process.env.REACT_APP_API_URL(ej:https://api.gamilit.com) - Request Interceptor: Agrega
Authorization: Bearer <JWT>automáticamente - Response Interceptor: Maneja errores 401 (redirect a login), errores de red
14. useAuth Hook
Tipo: Context Hook
Archivo: apps/frontend/src/contexts/AuthContext.tsx
Gap: STUDENT-GAP-006, STUDENT-GAP-007
ES CONSUMIDO POR (Dependencias Entrantes)
| # | Componente | Propiedades Usadas | Propósito | Frecuencia |
|---|---|---|---|---|
| 14.1 | ProfilePage Component | user.id, logout() |
Obtener ID del usuario + opción de logout | 1x por mount |
| 14.2 | SettingsPage Component | user.id |
Obtener ID del usuario para editar perfil | 1x por mount |
| 14.3 | useUserStatistics Hook | user.id (vía ProfilePage) |
Construir URL del endpoint | Indirecto |
📊 MATRIZ DE ACOPLAMIENTO
Nivel de Acoplamiento por Componente
| Componente | Dependencias Salientes | Dependencias Entrantes | Nivel de Acoplamiento | Evaluación |
|---|---|---|---|---|
| MissionsService | 4 | 2 | Alto | ⚠️ Refactorizar si crece |
| MissionsController | 3 | 2 | Medio | ✅ Aceptable |
| AuthService.getUserStatistics() | 6 | 2 | Alto | ⚠️ Queries a 6 tablas (aceptable para stats) |
| UsersController | 2 | 2 | Bajo | ✅ Excelente |
| useUserStatistics | 3 | 1 | Bajo | ✅ Excelente |
| ProfilePage | 3 | 1 | Bajo | ✅ Excelente |
| profileAPI | 5 | 1 | Medio-Alto | ⚠️ Muchos endpoints |
| SettingsPage | 4 | 1 | Medio | ✅ Aceptable |
Interpretación:
- Bajo (1-2): Excelente - Fácil de mantener y testear
- Medio (3-4): Aceptable - Atención a cambios
- Alto (5+): ⚠️ Refactorizar - Difícil de mantener
🔄 DIAGRAMAS DE FLUJO DE DATOS
Flujo: Reclamar Misión (GAP-001)
[Student UI]
↓ (1) Click "Reclamar"
[MissionsPage.tsx]
↓ (2) useClaimMissionRewards.mutate(missionId)
[apiClient]
↓ (3) POST /api/missions/:id/claim (+ JWT token)
[MissionsController]
↓ (4) claimMissionRewards(missionId, userId)
[MissionsService]
├─ (5a) SELECT mission (missionsRepository)
├─ (5b) Validate mission completed & not claimed
├─ (6a) getCurrentRank(userId) → previousRank
├─ (7a) MLCoinsService.addCoins() → INSERT ml_coins_transactions
├─ (7b) UserStatsService.addXp() → UPDATE user_stats.total_xp
├─ (7c) [Trigger] check_user_promotion() → UPDATE user_ranks (si aplica)
├─ (6b) getCurrentRank(userId) → newRank
├─ (8) Compare previousRank vs newRank → detect promotion
├─ (9) UPDATE mission (status='claimed', claimed_at=NOW)
└─ (10) Return mission + rewards_granted
[Frontend]
├─ (11) Invalidate ['missions'] query cache
├─ (12) Show toast: "Recompensas reclamadas: +100 XP, +50 coins"
└─ (13) If rank_promotion: Show toast: "¡Promoción! Ahora eres Nacom"
Flujo: Cargar Perfil (GAP-006)
[Student UI]
↓ (1) Navigate to /student/profile
[React Router]
↓ (2) Mount ProfilePage component
[ProfilePage]
├─ (3) useAuth() → get user.id
└─ (4) useUserStatistics(user.id) → React Query
├─ (5) Check cache: Has ['userStatistics', user.id]?
│ ├─ YES + fresh (< 2 min) → (6a) Return cached data (skip fetch)
│ └─ NO or stale → (6b) Execute queryFn
├─ (7) apiClient.get('/users/user-123/statistics') (+ JWT)
└─ (8) Backend: GET /users/:id/statistics → UsersController
├─ (9) AuthService.getUserStatistics() → 6 queries reales (GAP-008)
│ ├─ Query 1: SUM ml_coins_transactions → balance
│ ├─ Query 2: SELECT user_stats → XP, modules, streak
│ ├─ Query 3: SELECT user_ranks (is_current=true) → rank
│ ├─ Query 4: COUNT user_achievements (is_completed=true) → earned
│ ├─ Query 5: COUNT achievements (is_active=true) → available
│ └─ Query 6: COUNT exercise_submissions (is_correct=true) → completed
├─ (10) Manejar edge cases (NULLs, usuarios nuevos, fallbacks)
└─ (11) Return UserStatistics object con datos reales
[React Query]
├─ (12) Cache response (key: ['userStatistics', 'user-123'], staleTime: 2min)
└─ (13) Trigger re-render with data
[ProfilePage]
├─ (14) Construct stats array from userStats
└─ (15) Render stats cards with real values
Flujo: Guardar Settings (GAP-007)
[Student UI]
↓ (1) Edit fields + Click "Guardar cambios"
[SettingsPage]
├─ (2) handleSave() async
├─ (3) setSaveStatus('saving') → Button shows "Guardando..." + Loader2
├─ (4) profileAPI.updateProfile(user.id, profileData)
│ ├─ (5) PUT /api/users/user-123/profile (+ JWT)
│ └─ (6) Backend: UsersController.updateProfile()
│ ├─ (7) Validate email unique
│ ├─ (8) UPDATE users.users SET first_name=..., last_name=..., email=...
│ └─ (9) Return updated user
├─ (10) profileAPI.updatePreferences(user.id, preferencesData)
│ ├─ (11) PUT /api/users/user-123/preferences (+ JWT)
│ └─ (12) Backend: UsersController.updatePreferences()
│ ├─ (13) UPDATE users.users SET preferences=... (JSONB)
│ └─ (14) Return updated user
├─ (15) [Success] setSaveStatus('saved') → Button shows "Guardado ✓"
├─ (16) toast.success('Configuración guardada correctamente')
└─ (17) setTimeout → setSaveStatus('idle') (after 2s)
[Error Path]
├─ (E1) [Catch] setSaveStatus('error') → Button shows "Error ✗"
├─ (E2) toast.error(error.response?.data?.message || 'Error al guardar')
└─ (E3) setTimeout → setSaveStatus('idle') (after 3s)
🎯 RECOMENDACIONES
Reducir Acoplamiento
-
MissionsService (4 dependencias salientes):
- Considerar patrón Event-Driven: Emitir evento
MissionClaimedEventen lugar de llamar directamente a MLCoinsService, UserStatsService - Beneficio: Otros listeners podrían reaccionar (ej: AchievementsService detecta "Completa 10 misiones")
- Implementación: NestJS EventEmitter
- Considerar patrón Event-Driven: Emitir evento
-
profileAPI (5 endpoints):
- Ya está bien estructurado (cada método = 1 endpoint)
- Mantener como está, no refactorizar innecesariamente
Mejorar Testabilidad
-
Agregar tests unitarios para componentes con alto acoplamiento:
MissionsService.claimRewards()- Mockear 4 dependenciasSettingsPage.handlePasswordChange()- Mockear profileAPI y validar lógica
-
Agregar tests de integración para flujos completos:
- Flujo: Reclamar misión → Validar XP/coins en BD
- Flujo: Cambiar contraseña → Validar login con nueva password
Monitorear Dependencias Externas
-
React Query:
- Considerar migrar a v5 si estamos en v4 (mejoras de performance)
- Configurar QueryClient global con defaults (staleTime, cacheTime)
-
react-hot-toast:
- Evaluar alternativas si hay problemas (ej: react-toastify)
- Considerar implementar toast queue para múltiples toasts simultáneos
✅ CONCLUSIÓN
Resumen de Dependencias
| Capa | Componentes Analizados | Dependencias Salientes (Avg) | Dependencias Entrantes (Avg) | Acoplamiento Promedio |
|---|---|---|---|---|
| Backend | 4 | 3.75 | 2.0 | Medio-Alto |
| Frontend | 4 | 3.5 | 1.0 | Medio-Bajo |
| Database | 10 | 0 | 1.5 | Bajo |
| External | 4 | 0 | 1.5 | Bajo |
Estado de Dependencias
- ✅ Arquitectura limpia: Separación de concerns bien definida
- ✅ Bajo acoplamiento frontend: Hooks reutilizables, componentes independientes
- ⚠️ Acoplamiento medio backend: MissionsService con 4 dependencias (acceptable)
- ✅ Dependencias externas bien gestionadas: Npm packages estables y populares
Próximos Pasos
-
✅ COMPLETADO (2025-11-24): Implementar backend real (GAP-008) ✅
- AuthService.getUserStatistics() con 6 queries reales
- Multi-schema integration (gamification, progress)
- Edge cases manejados
-
Importante (P1): Agregar tests unitarios (mockear dependencias)
- AuthService.getUserStatistics() - mockear 6 repositorios
- MissionsService.claimRewards() - mockear 4 dependencias
- useUserStatistics hook - testing con React Query
-
Mejora (P2): Considerar Event-Driven architecture para reducir acoplamiento
- Emitir MissionClaimedEvent en lugar de llamar directamente a servicios
- Permite extensibilidad sin modificar MissionsService
-
Pendiente (P2): Implementar PUT/POST endpoints para Settings
- updateProfile(), updatePreferences(), uploadAvatar(), updatePassword()
- Frontend ya implementado (GAP-007)
Matriz generada: 2025-11-24 Última actualización: 2025-11-24 (Post-GAP-008) Versión: 1.1.0 Estado: ✅ COMPLETADO