workspace/projects/gamilit/docs/01-fase-alcance-inicial/EAI-003-gamificacion/historias-usuario/US-GAM-008-recompensas-modulos.md
rckrdmrd ea1879f4ad feat: Initial workspace structure with multi-level Git configuration
- 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>
2025-12-08 10:44:23 -06:00

11 KiB

US-GAM-008: Recompensas por completar módulos

Épica: EAI-003 - Gamificación Básica Sprint: Mes 1, Semana 4 Story Points: 5 SP Presupuesto: $1,800 MXN Prioridad: Alta (Alcance Inicial) Estado: Completada (Mes 1)


Descripción

Como estudiante, quiero recibir recompensas especiales al completar módulos para sentir logro por terminar secciones completas del contenido.

Contexto del Alcance Inicial: Recompensas fijas (XP, monedas, insignia) al completar cada módulo. Los valores están hardcoded en la definición de cada módulo.


Criterios de Aceptación

  • CA-01: Al completar un módulo, se otorgan: XP fijo, Monedas fijas, Insignia opcional
  • CA-02: Valores mayores que actividades individuales (ej: 50 XP, 25 monedas)
  • CA-03: Modal de felicitación muestra todas las recompensas
  • CA-04: Se registra fecha de completitud del módulo
  • CA-05: Dashboard muestra módulos completados
  • CA-06: Badge de "Módulo completado" si aplica
  • CA-07: No se puede reclamar recompensa dos veces

Especificaciones Técnicas

Backend

@Entity('modules')
class Module {
  // ... campos previos

  @Column({ type: 'int', default: 50 })
  completionXP: number

  @Column({ type: 'int', default: 25 })
  completionCoins: number

  @Column({ nullable: true })
  completionBadgeId?: string // Badge especial por completar este módulo
}

@Entity('module_completion')
class ModuleCompletion {
  @PrimaryGeneratedColumn('uuid')
  id: string

  @ManyToOne(() => User)
  user: User

  @Column()
  userId: string

  @ManyToOne(() => Module)
  module: Module

  @Column()
  moduleId: string

  @Column({ type: 'int' })
  xpAwarded: number

  @Column({ type: 'int' })
  coinsAwarded: number

  @Column({ nullable: true })
  badgeAwarded?: string

  @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' })
  completedAt: Date
}

class ModulesService {
  async completeModule(userId: string, moduleId: string) {
    // Verificar que no esté ya completado
    const existingCompletion = await this.moduleCompletionRepository.findOne({
      where: { userId, moduleId }
    })

    if (existingCompletion) {
      throw new BadRequestException('Module already completed')
    }

    // Verificar que todas las actividades estén completadas
    const progress = await this.getModuleProgress(moduleId, userId)
    if (progress.progressPercentage < 100) {
      throw new BadRequestException('Not all activities completed')
    }

    const module = await this.modulesRepository.findOne({ where: { id: moduleId } })

    // Otorgar recompensas
    await this.xpService.awardXP(userId, module.completionXP, 'module_completed', moduleId)
    await this.coinsService.awardCoins(userId, module.completionCoins, 'module', moduleId)

    let badgeAwarded = null
    if (module.completionBadgeId) {
      const badge = await this.badgesService.awardBadge(userId, module.completionBadgeId)
      badgeAwarded = badge.id
    }

    // Registrar completitud
    await this.moduleCompletionRepository.save({
      userId,
      moduleId,
      xpAwarded: module.completionXP,
      coinsAwarded: module.completionCoins,
      badgeAwarded,
      completedAt: new Date()
    })

    // Verificar logros adicionales (ej: primer módulo completado)
    await this.badgesService.checkAndAwardBadges(userId, 'module_completed', { moduleId })

    return {
      moduleCompleted: true,
      rewards: {
        xp: module.completionXP,
        coins: module.completionCoins,
        badge: badgeAwarded
      }
    }
  }

  async getCompletedModules(userId: string) {
    const completions = await this.moduleCompletionRepository.find({
      where: { userId },
      relations: ['module'],
      order: { completedAt: 'DESC' }
    })

    return completions.map(c => ({
      moduleId: c.moduleId,
      moduleName: c.module.title,
      completedAt: c.completedAt,
      xpAwarded: c.xpAwarded,
      coinsAwarded: c.coinsAwarded
    }))
  }
}

Endpoints:

POST /api/modules/:moduleId/complete
- Response: {
    moduleCompleted: true,
    rewards: { xp, coins, badge }
  }

GET /api/modules/completed
- Response: {
    completedModules: [
      { moduleId, moduleName, completedAt, xpAwarded, coinsAwarded }
    ],
    totalCompleted: number
  }

Frontend

Ya implementado en US-ACT-008 (ModuleCompletionModal), pero se extiende:

// components/modules/ModuleCompletionModal.tsx (actualizado)
export function ModuleCompletionModal({ module, rewards, onClose }) {
  useEffect(() => {
    confetti({
      particleCount: 200,
      spread: 180,
      origin: { y: 0.3 }
    })
  }, [])

  return (
    <Modal isOpen onClose={onClose} size="lg">
      <div className="text-center py-8">
        <div className="text-6xl mb-4">🎉</div>

        <h2 className="text-3xl font-bold text-gray-900 mb-3">
          ¡Módulo Completado!
        </h2>

        <p className="text-lg text-gray-600 mb-6">
          Has completado exitosamente "{module.title}"
        </p>

        {/* Recompensas */}
        <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
          {/* XP */}
          <div className="bg-yellow-50 p-6 rounded-lg border-2 border-yellow-200">
            <div className="text-4xl mb-2"></div>
            <p className="text-sm text-gray-600">Experiencia</p>
            <p className="text-3xl font-bold text-yellow-700">
              +{rewards.xp}
            </p>
            <p className="text-xs text-gray-500">XP</p>
          </div>

          {/* Monedas */}
          <div className="bg-gold-50 p-6 rounded-lg border-2 border-gold-200">
            <div className="text-4xl mb-2">💰</div>
            <p className="text-sm text-gray-600">ML Coins</p>
            <p className="text-3xl font-bold text-gold-700">
              +{rewards.coins}
            </p>
            <p className="text-xs text-gray-500">monedas</p>
          </div>

          {/* Insignia */}
          {rewards.badge && (
            <div className="bg-purple-50 p-6 rounded-lg border-2 border-purple-200">
              <div className="text-4xl mb-2">🏆</div>
              <p className="text-sm text-gray-600">Insignia</p>
              <img
                src={rewards.badge.imageUrl}
                alt={rewards.badge.name}
                className="w-12 h-12 mx-auto mt-2"
              />
              <p className="text-xs text-purple-700 mt-1">{rewards.badge.name}</p>
            </div>
          )}
        </div>

        {/* Mensaje motivacional */}
        <div className="bg-blue-50 border-l-4 border-blue-500 p-4 mb-6">
          <p className="text-blue-800 italic">
            "Has dado un gran paso en tu camino del conocimiento maya. Continúa explorando y descubriendo los secretos de esta fascinante civilización."
          </p>
          <p className="text-sm text-blue-600 mt-2">- Ixchel, Guardiana del Conocimiento</p>
        </div>

        {/* Botones */}
        <div className="flex gap-3">
          <Button onClick={() => window.location.href = '/modules'} variant="outline" fullWidth>
            Ver más módulos
          </Button>
          <Button onClick={onClose} variant="primary" fullWidth>
            Volver al Dashboard
          </Button>
        </div>
      </div>
    </Modal>
  )
}

// Dashboard - Módulos completados
export function CompletedModulesSection({ completedModules }) {
  return (
    <Card>
      <h3 className="font-bold text-lg mb-3">Módulos Completados</h3>

      {completedModules.length === 0 ? (
        <p className="text-gray-500">Aún no has completado ningún módulo. ¡Comienza tu aventura!</p>
      ) : (
        <div className="space-y-2">
          {completedModules.map(module => (
            <div
              key={module.moduleId}
              className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-200"
            >
              <div className="flex items-center gap-3">
                <div className="text-2xl"></div>
                <div>
                  <p className="font-medium text-gray-900">{module.moduleName}</p>
                  <p className="text-xs text-gray-600">
                    Completado: {new Date(module.completedAt).toLocaleDateString()}
                  </p>
                </div>
              </div>

              <div className="text-right text-sm">
                <p className="text-yellow-700">+{module.xpAwarded} XP</p>
                <p className="text-gold-700">+{module.coinsAwarded} 💰</p>
              </div>
            </div>
          ))}
        </div>
      )}
    </Card>
  )
}

Seed Data - Módulos con recompensas

const MODULES_SEED = [
  {
    id: 'modulo-numeros-mayas',
    title: 'Números Mayas',
    description: 'Aprende el sistema numérico vigesimal maya',
    completionXP: 50,
    completionCoins: 25,
    completionBadgeId: 'badge-numeros-master' // Badge específico
  },
  {
    id: 'modulo-calendario-haab',
    title: 'Calendario Haab',
    description: 'Descubre el calendario solar de 365 días',
    completionXP: 60,
    completionCoins: 30,
    completionBadgeId: 'badge-calendario-master'
  },
  {
    id: 'modulo-astronomia-maya',
    title: 'Astronomía Maya',
    description: 'Explora los conocimientos astronómicos',
    completionXP: 75,
    completionCoins: 40,
    completionBadgeId: null // Sin badge específico
  }
]

Dependencias

Antes:

  • US-ACT-008 (Navegación actividades)
  • US-GAM-002 (XP)
  • US-GAM-003 (Monedas)
  • US-GAM-005 (Insignias)

Definición de Hecho (DoD)

  • Sistema de recompensas implementado
  • No se puede reclamar dos veces
  • Modal muestra todas las recompensas
  • Dashboard lista módulos completados
  • Tests unitarios
  • Validación de completitud

Notas del Alcance Inicial

  • Recompensas fijas por módulo
  • XP y monedas hardcoded
  • Badge opcional por módulo
  • Sin bonificaciones por velocidad de completitud
  • ⚠️ Extensión futura: EXT-029-DynamicRewards (bonos por tiempo, precisión, racha)

Testing

describe('ModulesService - Completion', () => {
  it('should award rewards on module completion')
  it('should not allow duplicate completion')
  it('should require 100% progress')
  it('should award completion badge if defined')
  it('should trigger badge checks for milestones')
})

Estimación

Desglose de Esfuerzo (5 SP = ~1.75 días):

  • Backend: lógica completitud: 0.75 días
  • Frontend: actualizar modal: 0.5 días
  • Dashboard section: 0.25 días
  • Testing: 0.25 días

Creado: 2025-11-02 Actualizado: 2025-11-02 Responsable: Equipo Fullstack