workspace/projects/gamilit/docs/03-fase-extensiones/EXT-003-notificaciones/historias-usuario/US-NOT-001c-preferences-management.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

13 KiB

US-NOT-001c: Gestión de Preferencias de Notificaciones

Épica: EXT-003 - Sistema de Notificaciones Sprint: Mes 3, Semana 3 Story Points: 4 SP Presupuesto: $5,260 MXN Prioridad: Alta (Extensión Fase 3) Estado: 📋 Planificada Relación: Parte de US-NOT-001 (dividida en a/b/c por PF-001)


Descripción

Como estudiante de Gamilit Quiero configurar mis preferencias de notificaciones (tipos, canales, sonidos, frecuencia) Para controlar qué notificaciones recibo, cómo las recibo y evitar notification fatigue, personalizando mi experiencia

Contexto: Esta user story es parte del sistema completo de notificaciones (EXT-003), dividida para cumplir con PF-001. Esta parte implementa el panel de configuración donde usuarios personalizan sus preferencias.

Alcance:

  • Panel de preferencias en Settings > Notifications
  • Configuración por tipo de notificación (Logros, Amigos, Gremios, Misiones, Sistema)
  • Configuración por canal (In-App, Email, Push)
  • Configuración de sonidos y vibraciones
  • Guardar y validar preferencias
  • API REST para persistencia de preferencias

Valor de Negocio

  • Reduce Notification Fatigue: Usuarios controlan volumen de notificaciones
  • Aumenta Engagement: Notificaciones relevantes = mayor CTR
  • Tasa de Opt-Out Total: <5% (usuarios que desactivan todas)
  • Preferencias Personalizadas: >60% usuarios modifican al menos 1 preferencia
  • Compliance: GDPR/CCPA requieren opt-in para emails y push

Criterios de Aceptación

CA-01: Panel de Preferencias de Notificaciones

Dado que un usuario accede a configuración Cuando navega a Settings > Notifications Entonces debe ver panel con:

1. Preferencias Generales (In-App):

  • Notificaciones en tiempo real (toggle ON/OFF)

    • Default: ON
    • Efecto: Desactiva WebSocket y toasts si OFF
  • Mostrar toasts (toggle ON/OFF)

    • Default: ON
    • Requiere: Notificaciones en tiempo real ON
  • Sonido de notificaciones (toggle ON/OFF)

    • Default: ON
    • Requiere: Toasts ON
  • Vibración en móviles (toggle ON/OFF)

    • Default: ON
    • Solo visible en mobile

2. Preferencias por Tipo:

Cada tipo tiene 3 opciones (radio buttons):

  • Todas: Recibir todas las notificaciones de este tipo
  • Solo importantes: Solo notificaciones de alta prioridad (P0/P1)
  • Ninguna: No recibir notificaciones de este tipo

Tipos configurables:

  • Achievements (Logros): Todas / Solo raros y épicos / Ninguno

    • Default: Todas
  • Amigos: Todas / Solo solicitudes / Ninguno

    • Default: Todas
  • Gremios: Todas / Solo desafíos e invitaciones / Ninguno

    • Default: Todas
  • Misiones: Todas / Solo completadas y por expirar / Ninguno

    • Default: Todas
  • Sistema: Todas / Solo importantes / Ninguno

    • Default: Todas (no desactivable completamente)

3. Email Notifications (Futuro - UI preparada):

  • Resumen diario (toggle) - Default: OFF
  • Notificaciones importantes inmediatas (toggle) - Default: OFF
  • Desactivar todos los emails (toggle) - Default: ON

4. Push Notifications (Futuro - Placeholder):

  • Habilitar push notifications (toggle)
    • Estado: "Próximamente disponible" (disabled en v1)
    • Backend preparado, frontend no implementado

Validación:

  • Panel carga preferencias actuales desde API
  • Cambios se guardan automáticamente (auto-save cada 2 segundos)
  • Mensaje de confirmación: "Preferencias guardadas" (toast verde)

CA-02: Persistencia de Preferencias

Dado que un usuario modifica preferencias Cuando cambia un toggle o selecciona opción Entonces debe:

Auto-Save:

  • Cambios guardados automáticamente cada 2 segundos (debounced)
  • Indicador visual: "Guardando..." → "Guardado ✓"
  • Request a API: PUT /api/user/notification-preferences
  • Validación en backend antes de persistir

Sincronización:

  • Preferencias aplicadas inmediatamente en WebSocket
  • Notificaciones filtradas según preferencias antes de enviar
  • Cache invalidada al guardar (React Query)

Validación:

  • Usuario cambia "Achievements" de "Todas" a "Solo raros" → guardado automáticamente
  • Cambios persisten al recargar página
  • Error 400 si intenta guardar valor inválido

CA-03: Aplicación de Preferencias en Tiempo Real

Dado que un usuario tiene preferencias configuradas Cuando se generan notificaciones Entonces sistema debe respetar preferencias:

Filtrado:

  • Backend consulta preferencias antes de enviar notificación
  • Si tipo desactivado (none) → no enviar
  • Si "solo importantes" → enviar solo P0/P1
  • Si toasts desactivados → no mostrar toast (solo centro)
  • Si sonido desactivado → toast sin sonido

Ejemplos:

  • Usuario tiene "Achievements" en "Solo raros" → Logro común NO se envía, logro raro SÍ
  • Usuario tiene "Sonido" en OFF → Toast se muestra sin sonido
  • Usuario tiene "Toasts" en OFF → Notificación en centro silenciosamente

Validación:

  • Usuario con "Amigos: Solo solicitudes" NO recibe "friend:online"
  • Usuario con "Amigos: Solo solicitudes" SÍ recibe "friend:request"

CA-04: API REST para Preferencias

Dado que se necesita persistir preferencias Cuando frontend realiza requests Entonces backend debe proveer endpoints:

Endpoints:

// Get user's notification preferences
GET /api/user/notification-preferences
Response: {
  inAppEnabled: boolean,
  toastEnabled: boolean,
  soundEnabled: boolean,
  vibrationEnabled: boolean,
  emailEnabled: boolean,
  pushEnabled: boolean,
  preferencesByType: {
    achievements: 'all' | 'important' | 'none',
    friends: 'all' | 'important' | 'none',
    guilds: 'all' | 'important' | 'none',
    missions: 'all' | 'important' | 'none',
    system: 'all' | 'important' // System no puede ser 'none'
  }
}

// Update preferences
PUT /api/user/notification-preferences
Request Body: NotificationPreferencesDTO
Response: { success: true, preferences: NotificationPreferences }

// Reset to default
POST /api/user/notification-preferences/reset
Response: { success: true, preferences: NotificationPreferences }

Validación:

  • GET retorna preferencias actuales del usuario
  • PUT guarda y retorna preferencias actualizadas
  • Request inválido retorna 400

Especificaciones Técnicas

Backend - API Endpoints

Tecnologías: NestJS, PostgreSQL, class-validator

PreferencesController:

import { Controller, Get, Put, Post, Body, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard';
import { CurrentUser } from '@/auth/decorators/current-user.decorator';

@Controller('user/notification-preferences')
@UseGuards(JwtAuthGuard)
export class NotificationPreferencesController {
  constructor(private readonly preferencesService: NotificationPreferencesService) {}

  @Get()
  async getPreferences(@CurrentUser() user) {
    return this.preferencesService.getPreferences(user.id);
  }

  @Put()
  async updatePreferences(@CurrentUser() user, @Body() dto: UpdatePreferencesDto) {
    const preferences = await this.preferencesService.updatePreferences(user.id, dto);
    return { success: true, preferences };
  }

  @Post('reset')
  async resetPreferences(@CurrentUser() user) {
    const preferences = await this.preferencesService.resetToDefaults(user.id);
    return { success: true, preferences };
  }
}

UpdatePreferencesDto:

import { IsBoolean, IsEnum, ValidateNested } from 'class-validator';

enum PreferenceLevel { ALL = 'all', IMPORTANT = 'important', NONE = 'none' }

class PreferencesByTypeDto {
  @IsEnum(PreferenceLevel) achievements: PreferenceLevel;
  @IsEnum(PreferenceLevel) friends: PreferenceLevel;
  @IsEnum(PreferenceLevel) guilds: PreferenceLevel;
  @IsEnum(PreferenceLevel) missions: PreferenceLevel;
  @IsEnum(['all', 'important']) system: 'all' | 'important'; // No 'none'
}

export class UpdatePreferencesDto {
  @IsBoolean() inAppEnabled: boolean;
  @IsBoolean() toastEnabled: boolean;
  @IsBoolean() soundEnabled: boolean;
  @IsBoolean() vibrationEnabled: boolean;
  @IsBoolean() emailEnabled: boolean;
  @IsBoolean() pushEnabled: boolean;
  @ValidateNested() preferencesByType: PreferencesByTypeDto;
}

Database Schema

CREATE TABLE notification_preferences (
  user_id UUID PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
  in_app_enabled BOOLEAN DEFAULT true,
  toast_enabled BOOLEAN DEFAULT true,
  sound_enabled BOOLEAN DEFAULT true,
  vibration_enabled BOOLEAN DEFAULT true,
  email_enabled BOOLEAN DEFAULT false,
  push_enabled BOOLEAN DEFAULT false,
  preferences_by_type JSONB DEFAULT '{
    "achievements": "all", "friends": "all", "guilds": "all",
    "missions": "all", "system": "all"
  }',
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

-- Auto-crear preferencias al crear usuario
CREATE FUNCTION create_default_notification_preferences()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO notification_preferences (user_id) VALUES (NEW.id);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_create_notification_preferences
AFTER INSERT ON users FOR EACH ROW
EXECUTE FUNCTION create_default_notification_preferences();

Frontend - React Components

Tecnologías: React 18, React Hook Form, Zod, TailwindCSS

NotificationPreferencesPage:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { useNotificationPreferences } from '@/hooks/useNotificationPreferences';

const preferencesSchema = z.object({
  inAppEnabled: z.boolean(),
  toastEnabled: z.boolean(),
  soundEnabled: z.boolean(),
  vibrationEnabled: z.boolean(),
  preferencesByType: z.object({
    achievements: z.enum(['all', 'important', 'none']),
    friends: z.enum(['all', 'important', 'none']),
    guilds: z.enum(['all', 'important', 'none']),
    missions: z.enum(['all', 'important', 'none']),
    system: z.enum(['all', 'important'])
  })
});

export function NotificationPreferencesPage() {
  const { preferences, updatePreferences } = useNotificationPreferences();
  const { register, watch } = useForm({
    resolver: zodResolver(preferencesSchema),
    defaultValues: preferences
  });

  // Auto-save on change (debounced)
  useEffect(() => {
    const subscription = watch((value) => {
      const timeoutId = setTimeout(() => updatePreferences(value), 2000);
      return () => clearTimeout(timeoutId);
    });
    return () => subscription.unsubscribe();
  }, [watch]);

  return (
    <div className="max-w-2xl mx-auto p-6">
      <h1 className="text-2xl font-bold mb-6">Preferencias de Notificaciones</h1>

      <section className="mb-8">
        <h2 className="text-lg font-semibold mb-4">Notificaciones In-App</h2>
        <Toggle label="Notificaciones en tiempo real" {...register('inAppEnabled')} />
        <Toggle label="Mostrar toasts" disabled={!watch('inAppEnabled')} {...register('toastEnabled')} />
        <Toggle label="Sonido" disabled={!watch('toastEnabled')} {...register('soundEnabled')} />
        <Toggle label="Vibración" {...register('vibrationEnabled')} />
      </section>

      <section>
        <h2 className="text-lg font-semibold mb-4">Preferencias por Tipo</h2>
        <RadioGroup
          label="Achievements"
          options={[
            { value: 'all', label: 'Todas' },
            { value: 'important', label: 'Solo raros y épicos' },
            { value: 'none', label: 'Ninguno' }
          ]}
          {...register('preferencesByType.achievements')}
        />
        {/* ... más tipos ... */}
      </section>
    </div>
  );
}

Dependencias

Requiere:

  • US-NOT-001a: Infraestructura WebSocket - aplica preferencias al enviar
  • Sistema de autenticación (AUTH-001)

Relacionada:

  • US-NOT-001b: Centro de Notificaciones - usa preferencias

Definición de Hecho (DoD)

Desarrollo

  • Panel de preferencias UI implementado
  • Toggles y radio buttons funcionales
  • Auto-save con debounce implementado
  • API endpoints REST implementados (GET, PUT, POST)
  • Validación de DTOs en backend
  • Schema de base de datos creado
  • Trigger de auto-creación de preferencias

Testing

  • Tests unitarios: PreferencesController, PreferencesService
  • Tests de integración: API endpoints
  • Tests E2E: Usuario cambia preferencias → se aplican
  • Tests de validación: DTOs rechazan valores inválidos

UX/UI

  • Auto-save con indicador visual
  • Tooltips explicativos
  • Responsive (mobile, desktop)

Estimación

  • Backend - API & Service: 8 horas
  • Backend - Validación & DB: 4 horas
  • Frontend - UI Components: 12 horas
  • Frontend - Form Logic & Auto-save: 4 horas
  • Testing: 4 horas

Total: 32 horas (4 SP @ 8h/SP)


Notas

  • Archivo modularizado desde US-NOT-001-FULL.md (2025-11-02)
  • Cumple PF-001 (<400L)
  • Implementar después de US-NOT-001a y US-NOT-001b
  • 📝 Email y Push son placeholders para Fase 4
  • 🎨 Auto-save cada 2 segundos mejora UX
  • ⚠️ Sistema (system) no puede desactivarse completamente

Stack Tecnológico

Frontend: React 18, React Hook Form, Zod, TailwindCSS Backend: NestJS, class-validator, PostgreSQL Testing: Jest, Supertest, Cypress


Tags: #preferences #settings #ui #backend #api #ext-003 #fase3